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

Моя программа часто требует выполнения следующих расчетов:

Дано:

  • N - это 32-разрядное целое число
  • D - это 32-разрядное целое число
  • abs (N) & lt; = abs (D)
  • D! = 0
  • X - это 32-разрядное целое число любого значения

Найти:

  • X * N/D как округленное целое число, то есть X, масштабированное до N/D (т.е. 10 * 2/3 = 7)

Очевидно, я мог бы просто использовать r=x*n/d напрямую, но я часто получаю переполнение от x*n. Если я вместо этого сделаю r=x*(n/d), то получу только 0 или x из-за целочисленного деления, отбрасывающего дробный компонент. И затем есть r=x*(float(n)/d), но я не могу использовать поплавки в этом случае.

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

N и D в настоящее время подписаны, но я могу обойтись без них, если это поможет.

Универсальная функция, которая работает с любым значением X (и N и D, если N & lt; = D), является идеальной, поскольку эта операция используется различными способами, но у меня также есть конкретный случай, когда значение X представляет собой известная постоянная сила 2 (2048, если быть точным), и просто ускорить этот конкретный вызов было бы большой помощью.

В настоящее время я выполняю это, используя 64-битное умножение и деление, чтобы избежать переполнения (по сути, int multByProperFraction(int x, int n, int d) { return (__int64)x * n / d; }, но с некоторыми утверждениями и дополнительными битами для округления вместо усечения).

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

Ответы

Ответ 1

Сейчас я протестировал несколько возможных решений, в том числе странные/умные из других источников, таких как объединение 32-битного div & мод & добавить или использовать крестьянскую математику, и вот мои выводы:

Во-первых, если вы ориентируетесь только на Windows и используете VSC++, просто используйте MulDiv(). Он довольно быстрый (быстрее, чем прямое использование 64-битных переменных в моих тестах), но при этом он также точен и округляет результат для вас. Я не смог найти какой-либо превосходный метод для такого рода действий в Windows с VSC++, даже с учетом ограничений, таких как только без знака и N & lt; = D.

Однако в моем случае наличие функции с детерминированными результатами даже на разных платформах даже важнее скорости. На другой платформе, которую я использовал в качестве теста, 64-разрядное деление намного, намного медленнее, чем 32-разрядное при использовании 32-разрядных библиотек, и нет функции MulDiv() для использования. 64-разрядное деление на этой платформе занимает ~ 26x столько же, сколько 32-разрядное деление (однако 64-разрядное умножение так же быстро, как и 32-разрядная версия...).

Поэтому, если у вас есть такой случай, как я, я поделюсь лучшими результатами, которые я получил, что оказалось просто оптимизацией ответа chux.

Оба метода, о которых я расскажу ниже, используют следующую функцию (хотя встроенные функции компилятора на самом деле помогли только с MSVC в Windows):

inline u32 bitsRequired(u32 val)
{
    #ifdef _MSC_VER
        DWORD r = 0;
        _BitScanReverse(&r, val | 1);
        return r+1;
    #elif defined(__GNUC__) || defined(__clang__)
        return 32 - __builtin_clz(val | 1);
    #else
        int r = 1;
        while (val >>= 1) ++r;
        return r;
    #endif
}

Теперь, если x - это константа размером 16 бит или меньше, и вы можете предварительно вычислить требуемые биты, я нашел лучшие результаты по скорости и точности этой функции:

u32 multConstByPropFrac(u32 x, u32 nMaxBits, u32 n, u32 d)
{
    //assert(nMaxBits == 32 - bitsRequired(x));
    //assert(n <= d);
    const int bitShift = bitsRequired(n) - nMaxBits;
    if( bitShift > 0 )
    {
        n >>= bitShift;
        d >>= bitShift;
    }

    // Remove the + d/2 part if don't need rounding
    return (x * n + d/2) / d;
}

На платформе с медленным 64-разрядным делением вышеуказанная функция работала в ~ 16,75 раза быстрее, чем return ((u64)x * n + d/2) / d;, и со средней точностью 99,999981% (сравнивая разницу в возвращаемом значении с ожидаемым в диапазоне х, т.е. возвращая + / -1 от ожидаемого, когда x равно 2048, будет 100 - (1/2048 * 100) = точность 99,95%) при тестировании его с миллионом или около того рандомизированных входных данных, где примерно половина из них обычно была бы переполнением. В худшем случае точность составила 99,951172%.

Для общего случая использования я нашел лучшие результаты из следующих (и без необходимости ограничивать загрузку N & lt; = D!):

u32 scaleToFraction(u32 x, u32 n, u32 d)
{
    u32 bits = bitsRequired(x);
    int bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    int sh = bitShift;
    x >>= bitShift;

    bits = bitsRequired(n);
    bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    sh += bitShift;
    n >>= bitShift;

    bits = bitsRequired(d);
    bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    sh -= bitShift;
    d >>= bitShift;

    // Remove the + d/2 part if don't need rounding
    u32 r = (x * n + d/2) / d;
    if( sh < 0 )
        r >>= (-sh);
    else //if( sh > 0 )
        r <<= sh;

    return r;
}

На платформе с медленным 64-разрядным делением вышеуказанная функция работала в ~ 18,5 раза быстрее, чем при использовании 64-разрядных переменных, со средним значением 99,999426% и точностью наихудшего случая 99,947479%.

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

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

Очевидно, нет гарантии, что кто-то еще получит аналогичные результаты на других платформах!

ОБНОВЛЕНИЕ: Заменить некоторые хитрые хаки с простым кодом, который на самом деле работает быстрее в любом случае, позволяя компилятору делать свою работу.

Ответ 2

Терпеть неточность и использовать 16 бит MSB из n,d,x

Algorithm
while (|n| > 0xffff) n/2, sh++
while (|x| > 0xffff) x/2, sh++
while (|d| > 0xffff) d/2, sh--
r = n*x/d  // A 16x16 to 32 multiply followed by a 32/16-bit divide.
shift r by sh.

Когда 64 bit деление стоит дорого, здесь может потребоваться 32-битное деление до/пост обработки, что, безусловно, будет большой частью ЦП.

Если компилятор не может заставить 32-битное /16-битное деление, пропустите шаг while (|d| > 0xffff) d/2, sh-- и выполните 32/32 деление.

Используйте математику без знака, насколько это возможно.

Ответ 3

Основной правильный подход к этому - просто (uint64_t)x*n/d. Это оптимальное допущение d является переменным и непредсказуемым. Но если d является константой или изменяется нечасто, вы можете предварительно сгенерировать константы так, чтобы точное деление на d можно было выполнить как умножение с последующим сдвигом битов. Хорошее описание алгоритма, примерно то, что GCC использует для преобразования деления на константу в умножение, приведено здесь:

http://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html

Я не уверен, насколько легко заставить это работать для деления "64/32" (то есть деления результата (uint64_t)x*n), но вы должны быть в состоянии просто разбить его на верхнюю и нижнюю части, если ничего больше.

Обратите внимание, что эти алгоритмы также доступны как libdivide.