Быстрое умножение / деление на 2 для поплавков и двойников (C / C++)

в программном обеспечении, которое я пишу, я делаю миллионы умножения или деления на 2 (или степени 2) моих значений. Я бы очень хотел, чтобы эти значения были int чтобы я мог получить доступ к операторам bitshift

int a = 1;
int b = a<<24

однако, я не могу, и не удваивается.

мой вопрос : поскольку существует стандартное представление двойников (знак, показатель, мантисса), есть ли способ играть с показателем, чтобы получить быстрый умножения / деления в степени 2?

я даже могу предположить, что количество битов будет исправлено (ПО будет работать на машинах, которые всегда будут иметь 64 бит удваивается)

P. S: и да, алгоритм в основном выполняет только эти операции. Это узкое место (оно уже многопоточное).

Edit: или я полностью ошибаюсь, и умные компиляторы уже оптимизируют вещи для меня?


временные результаты (с Qt для измерения времени, перебор, но мне все равно):

#include <QtCore/QCoreApplication>
#include <QtCore/QElapsedTimer>
#include <QtCore/QDebug>

#include <iostream>
#include <math.h>

using namespace std;

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

while(true)
{
    QElapsedTimer timer;
    timer.start();

    int n=100000000;
    volatile double d=12.4;
    volatile double D;
    for(unsigned int i=0; i<n; ++i)
    {
        //D = d*32;      // 200 ms
        //D = d*(1<<5);  // 200 ms
        D = ldexp (d,5); // 6000 ms
    }

    qDebug() << "The operation took" << timer.elapsed() << "milliseconds";
}

return a.exec();
}

Runs предполагают, что D = d*(1<<5); и D = d*32; запустить в то же время (200 мс), тогда как D = ldexp (d,5); намного медленнее (6000 МС). Я!--15-->знаю что это микро-бенчмарк, и что внезапно моя оперативная память взорвалась, потому что Chrome внезапно попросил вычислить Pi в моей спине каждый раз, когда я запускаю ldexp(), так что этот тест ничего не стоит. Но я все равно оставлю его себе.

на другой, У меня проблемы с этим!--7--> потому что const нарушение (кажется volatile ключевое слово мешает)

8 ответов


вы можете довольно безопасно предположить форматирование IEEE 754, детали которого могут получить довольно gnarley (esp. когда вы попадаете в умственно отсталые). Однако в обычных случаях это должно работать:

const int DOUBLE_EXP_SHIFT = 52;
const unsigned long long DOUBLE_MANT_MASK = (1ull << DOUBLE_EXP_SHIFT) - 1ull;
const unsigned long long DOUBLE_EXP_MASK = ((1ull << 63) - 1) & ~DOUBLE_MANT_MASK; 
void unsafe_shl(double* d, int shift) { 
    unsigned long long* i = (unsigned long long*)d; 
    if ((*i & DOUBLE_EXP_MASK) && ((*i & DOUBLE_EXP_MASK) != DOUBLE_EXP_MASK)) { 
        *i += (unsigned long long)shift << DOUBLE_EXP_SHIFT; 
    } else if (*i) {
        *d *= (1 << shift);
    }
} 

EDIT: после некоторого времени этот метод странно медленнее, чем метод double на моем компиляторе и машине, даже разделенный до минимального выполненного кода:

    double ds[0x1000];
    for (int i = 0; i != 0x1000; i++)
        ds[i] = 1.2;

    clock_t t = clock();

    for (int j = 0; j != 1000000; j++)
        for (int i = 0; i != 0x1000; i++)
#if DOUBLE_SHIFT
            ds[i] *= 1 << 4;
#else
            ((unsigned int*)&ds[i])[1] += 4 << 20;
#endif

    clock_t e = clock();

    printf("%g\n", (float)(e - t) / CLOCKS_PER_SEC);

в DOUBLE_SHIFT завершается за 1,6 секунды, с внутренним циклом

movupd xmm0,xmmword ptr [ecx]  
lea    ecx,[ecx+10h]  
mulpd  xmm0,xmm1  
movupd xmmword ptr [ecx-10h],xmm0

Versus 2.4 секунды в противном случае, с внутренним циклом:

add dword ptr [ecx],400000h
lea ecx, [ecx+8]  

действительно неожиданно!

EDIT 2: Тайна решена! Одно из изменений для VC11 теперь всегда векторизует циклы с плавающей запятой, эффективно заставляя /arch:SSE2, хотя VC10, даже с /arch: SSE2 все еще хуже с 3.0 секундами с внутренним циклом:

movsd xmm1,mmword ptr [esp+eax*8+38h]  
mulsd xmm1,xmm0  
movsd mmword ptr [esp+eax*8+38h],xmm1  
inc   eax

VC10 без / arch:SSE2 (даже с /arch: SSE) составляет 5,3 секунды... С 1/100th итераций!! внутренняя петля:

fld         qword ptr [esp+eax*8+38h]  
inc         eax  
fmul        st,st(1)  
fstp        qword ptr [esp+eax*8+30h]

Я знал, что стек x87 FP был тяжелым,но в 500 раз хуже, это смешно. Вы, вероятно, не увидите, как эти виды ускорений преобразуются, т. е. матричные ops в SSE или int-хаки, так как это худший случай загрузки в стек FP, выполнения одного op и хранения из него, но это хороший пример того, почему x87-это не способ пойти на что-либо perf. связанный.


Это одна из тех специфических вещей с высоким уровнем применения. В одних случаях это может помочь, а в других-нет. (В подавляющем большинстве случаев прямое умножение по-прежнему лучше всего.)

"интуитивный" способ сделать это-просто извлечь биты в 64-разрядное целое число и добавить значение сдвига непосредственно в экспоненту. (это будет работать до тех пор, пока вы не нажмете NAN или INF)

что-то вроде этого:

union{
    uint64 i;
    double f;
};

f = 123.;
i += 0x0010000000000000ull;

//  Check for zero. And if it matters, denormals as well.

обратите внимание, что этот код не c-совместимый в любом случае, и показана только для иллюстрации идеи. Любая попытка реализовать это должна быть сделана непосредственно в сборке или встроенных компонентах SSE.

однако, в большинство случаи накладные расходы на перемещение данных из единицы FP в целочисленную единицу (и обратно) будут стоить намного больше, чем просто прямое умножение. Это особенно относится к pre-SSE era, где значение должно храниться из FPU x87 в память, а затем прочитайте обратно в целочисленные регистры.

в эпоху SSE целочисленные SSE и FP SSE используют одни и те же регистры ISA (хотя у них все еще есть отдельные файлы регистров). По Агнер Туман, существует штраф от 1 до 2 циклов для перемещения данных между целочисленными единицами выполнения SSE и FP SSE. Таким образом, стоимость намного лучше, чем в эпоху x87, но она все еще существует.

All-in-all, это будет зависеть от того, что еще у вас есть на вашем конвейере. Но в большинстве случаев умножение все равно будет быстрее. Я столкнулся с этой же проблемой раньше, поэтому я говорю из первых рук.

теперь с 256-битными инструкциями AVX, которые поддерживают только инструкции FP, еще меньше стимула играть в такие трюки.


как о ldexp?

любой мало-мальски приличный компилятор будет генерировать оптимальный код на вашей платформе.

но, как указывает @Clinton, просто написать его "очевидным" способом должно быть так же хорошо. Умножение и деление на степени двух-детская игра для современного компилятора.

непосредственно munging представление с плавающей запятой, помимо того, что не переносится, почти наверняка не будет быстрее (и вполне может быть замедлившийся.)

и, конечно, вы не должны тратить время даже думать об этом вопросе, если ваш инструмент профилирования не говорит вам. Но люди, которые прислушиваются к этому совету, никогда не будут нуждаться в нем, а те, кому он нужен, никогда не будут слушать.

[обновление]

хорошо, поэтому я просто попробовал ldexp с g++ 4.5.2. The cmath заголовок inlines это как вызов __builtin_ldexp, что, в свою очередь...

...испускает вызов libm


самый быстрый способ сделать это, вероятно:

x *= (1 << p);

такого рода вещи можно просто сделать, вызвав машинную инструкцию для добавления p экспоненты. Указание компилятору вместо этого извлечь некоторые биты с маской и сделать что-то вручную, вероятно, сделает вещи медленнее, а не быстрее.

помните, что C / C++ не является языком сборки. Использование оператора bitshift не обязательно компилируется в операцию сборки bitshift, но не использует умножение обязательно компилируется в умножение. Там происходит много странных и замечательных вещей, например, какие регистры используются и какие инструкции можно запускать одновременно, которые я недостаточно умен, чтобы понять. Но ваш компилятор, обладающий многими человеческими годами знаний и опыта и большой вычислительной мощью, намного лучше делает эти суждения.

п.з. имейте в виду, если ваш парный в массиве или другой структуре данных , ваш компилятор может быть очень умным и использовать SSE для нескольких 2 или даже 4 двойников одновременно. Однако выполнение большого сдвига битов, вероятно, запутает ваш компилятор и предотвратит эту оптимизацию.


какие еще операции требует этот алгоритм? Вы можете разбить свои поплавки на пары int (знак/мантисса и величина), выполнить обработку и восстановить их в конце.


умножение на 2 может быть заменено добавлением:x *= 2 эквивалентно x += x.

деление на 2 может быть заменено умножением на 0,5. Умножение обычно происходит значительно быстрее, чем деление.


хотя есть мало/нет практической пользы для лечения полномочий двух специально для поплавка двойных типов есть случай для этого для дабл-дабл типы. Двойное-двойное умножение и деление вообще сложны, но тривиальны для умножения и деления на степень два.

Е. Г. для

typedef struct {double hi; double lo;} doubledouble;
doubledouble x;
x.hi*=2, x.lo*=2; //multiply x by 2
x.hi/=2, x.lo/=2; //divide x by 2

на самом деле я перегружен << и >> на doubledouble так что это аналог числа.

//x is a doubledouble type
x << 2 // multiply x by four;
x >> 3 // divide x by eight.

в зависимости от того, что вы умножаете, если у вас есть данные, которые достаточно повторяющиеся, таблица поиска может обеспечить лучшую производительность за счет памяти.