Что быстрее на деление? удваивается / плавает / UInt32 / UInt64? в C ++ / C - PullRequest
2 голосов
/ 18 ноября 2009

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

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

Если мой тест по-прежнему бесполезен, дайте мне знать, но будьте добры, увидев, как я потратил два часа на написание этого дерьма: P

64 time: 3826718 us
32 time: 2476484 us
D(mul) time: 936524 us
D(div) time: 3614857 us
S time: 1506020 us

«Умножение на деление» с использованием двойных представляется самым быстрым способом деления с последующим целочисленным делением. Я не проверял точность деления. Может ли быть так, что «правильное деление» является более точным? У меня нет желания выяснять результаты этих тестов на скорость, так как я просто буду использовать целочисленное деление на константе с основанием 10 и позволю моему компилятору оптимизировать его для меня;) (и не побеждать его оптимизации).

Вот код, который я использовал для получения результатов:

#include <iostream>

int Run(int bla, int div, int add, int minus) {
    // these parameters are to force the compiler to not be able to optimise away the
    // multiplications and divides :)
    long LoopMax = 100000000;

    uint32_t Origbla32 = 1000000000;
    long i = 0;

    uint32_t bla32 = Origbla32;
    uint32_t div32 = div;
    clock_t Time32 = clock();
    for (i = 0; i < LoopMax; i++) {
        div32 += add;
        div32 -= minus;
        bla32 = bla32 / div32;
        bla32 += bla;
        bla32 = bla32 * div32;
    }
    Time32 = clock() - Time32;

    uint64_t bla64 = bla32;
    clock_t Time64 = clock();
    uint64_t div64 = div;
    for (long i = 0; i < LoopMax; i++) {
        div64 += add;
        div64 -= minus;
        bla64 = bla64 / div64;
        bla64 += bla;
        bla64 = bla64 * div64;
    }
    Time64 = clock() - Time64;

    double blaDMul = Origbla32;
    double multodiv = 1.0 / (double)div;
    double multomul = div;
    clock_t TimeDMul = clock();
    for (i = 0; i < LoopMax; i++) {
        multodiv += add;
        multomul -= minus;
        blaDMul = blaDMul * multodiv;
        blaDMul += bla;
        blaDMul = blaDMul * multomul;
    }
    TimeDMul = clock() - TimeDMul;

    double blaDDiv = Origbla32;
    clock_t TimeDDiv = clock();
    for (i = 0; i < LoopMax; i++) {
        multodiv += add;
        multomul -= minus;
        blaDDiv = blaDDiv / multomul;
        blaDDiv += bla;
        blaDDiv = blaDDiv / multodiv;
    }
    TimeDDiv = clock() - TimeDDiv;

    float blaS = Origbla32;
    float divS = div;
    clock_t TimeS = clock();
    for (i = 0; i < LoopMax; i++) {
        divS += add;
        divS -= minus;
        blaS = blaS / divS;
        blaS += bla;
        blaS = blaS * divS;
    }
    TimeS = clock() - TimeS;

    printf("64 time: %i us  (%i)\n", (int)Time64, (int)bla64);
    printf("32 time: %i us  (%i)\n", (int)Time32, bla32);

    printf("D(mul) time: %i us  (%f)\n", (int)TimeDMul, blaDMul);
    printf("D(div) time: %i us  (%f)\n", (int)TimeDDiv, blaDDiv);
    printf("S time: %i us  (%f)\n", (int)TimeS, blaS);

    return 0;
}

int main(int argc, char* const argv[]) {
    Run(0, 10, 0, 0); // adds and minuses 0 so it doesn't affect the math, only kills the opts
    return 0;
}

Ответы [ 5 ]

9 голосов
/ 18 ноября 2009

Есть много способов выполнить определенную арифметику, поэтому может не быть единственного ответа (сдвиг, дробное умножение, фактическое деление, некоторое обратное прохождение через единицу логарифма и т. Д .; все они могут иметь разные относительные затраты в зависимости от операнды и распределение ресурсов).

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

Для некоторых данных, применимых к сборке на x86, вы можете посмотреть: "Задержка команд и пропускная способность для процессоров AMD и Intel x86"

4 голосов
/ 18 ноября 2009

Что будет самым быстрым, будет полностью зависеть от целевой архитектуры. Похоже, вы интересуетесь только той платформой, на которой вы оказались, и, судя по вашим временам исполнения, это 64-битная версия x86, либо Intel (Core2?), Либо AMD.

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

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

Поскольку вас интересуют временные параметры на конкретной машине, вы должны знать, что Intel теперь публикует эту информацию в своем Справочном руководстве по оптимизации (pdf) . В частности, вас заинтересуют таблицы Приложения 3.1, раздел 3.1, «Задержка и пропускная способность с операндами регистра».

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

2 голосов
/ 18 ноября 2009

Как уже упоминал Стивен, используйте руководство по оптимизации - но вы также должны подумать об использовании инструкций SSE. Они могут выполнять 4 или 8 делений / умножений в одной инструкции.

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

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

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

0 голосов
/ 18 ноября 2009

Агнер Фог сам провел несколько довольно подробных измерений, которые можно найти здесь . Если вы действительно пытаетесь оптимизировать материал, вам следует прочитать остальные документы из его ресурсов по оптимизации программного обеспечения .

Я хотел бы отметить, что даже если вы измеряете не векторизованные операции с плавающей запятой, компилятор имеет две опции для сгенерированной сборки: он может использовать инструкции FPU (fadd, fmul) или он может использовать Инструкции SSE все еще манипулируют одним значением с плавающей запятой для каждой инструкции (addss, mulss). По моему опыту, инструкции SSE быстрее и имеют меньше неточностей, но компиляторы не делают его по умолчанию, потому что это может нарушить совместимость с кодом, который опирается на старое поведение. Вы можете включить его в gcc с флагом -mfpmath=sse.

0 голосов
/ 18 ноября 2009

Я написал некорректный тест, чтобы сделать это на MSVC 2008

double i32Time  = GetTime();
{
    volatile __int32 i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 61;
        count++;
    }
}
i32Time = GetTime() - i32Time;

double i64Time  = GetTime();
{
    volatile __int64 i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 61;
        count++;
    }
}
i64Time = GetTime() - i64Time;


double fTime    = GetTime();
{
    volatile float i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 4.0f;
        count++;
    }
}
fTime   = GetTime() - fTime;

double fmTime   = GetTime();
{
    volatile float i = 4;
    const float div = 1.0f / 4.0f;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i *= div;
        count++;
    }
}
fmTime  = GetTime() - fmTime;

double dTime    = GetTime();
{
    volatile double i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 4.0f;
        count++;
    }
}
dTime   = GetTime() - dTime;

double dmTime   = GetTime();
{
    volatile double i = 4;
    const double div = 1.0f / 4.0f;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i *= div;
        count++;
    }
}
dmTime  = GetTime() - dmTime;


DebugOutput( _T( "%f\n" ), i32Time );
DebugOutput( _T( "%f\n" ), i64Time );
DebugOutput( _T( "%f\n" ), fTime );
DebugOutput( _T( "%f\n" ), fmTime );
DebugOutput( _T( "%f\n" ), dTime );
DebugOutput( _T( "%f\n" ), dmTime );

DebugBreak();

Затем я запустил его на AMD64 Turion 64 в 32-битном режиме. Результаты, которые я получил, были следующими:

0.006622
0.054654
0.006283
0.006353
0.006203
0.006161

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

Это также категорически показывает, что компилятор MSVC выполняет умножение путем взаимной оптимизации. Я думаю, что GCC делает то же самое, если не лучше. Если я изменяю поплавки и двойные проверки деления на «i», то это значительно увеличивает время. Хотя многое из этого может быть связано с повторной загрузкой с диска, очевидно, что компилятор не может так легко это оптимизировать.

Чтобы понять такие микрооптимизации, попробуйте прочитать этот pdf.

В целом, я бы сказал, что если вы беспокоитесь о таких вещах, то, очевидно, вы не профилировали свой код. Профилируйте и устраняйте проблемы, как и когда они действительно являются проблемой.

...