Эффективное умножение / деление двух 128-битных целых чисел на x86 (без 64-битной) - PullRequest
9 голосов
/ 08 января 2012

Компилятор: MinGW / GCC
Проблемы: Не допускается код GPL / LGPL (GMP или любая другая библиотека bignum в этом отношении излишни для этой проблемы, поскольку у меня уже есть реализованный класс).

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


Когда дело доходит до операторов умножения и деления, они невыносимо медленны по сравнению со всем остальным в классе.

Это приблизительные измерения относительно моего собственного компьютера:

Raw times as defined by QueryPerformanceFrequency:
1/60sec          31080833u
Addition:              ~8u
Subtraction:           ~8u
Multiplication:      ~546u
Division:           ~4760u (with maximum bit count)

Как видите, простое умножение намного, во много раз медленнее, чем сложение или вычитание. Деление примерно в 10 раз медленнее, чем умножение.

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


Структура (методы опущены) выглядит примерно так:

class uint128_t
{
    public:
        unsigned long int dw3, dw2, dw1, dw0;
  //...
}

Умножение в настоящее время выполняется с использованием типичного метода long-умножения (в сборке, чтобы я мог перехватить вывод EDX), игнорируя слова, выходящие за пределы диапазона ( то есть я делаю только 10 mull по сравнению с 16).

Деление использует алгоритм shift-subtract (скорость зависит от количества битов операндов). Однако, это не сделано в сборке. Я нашел, что это слишком сложно собрать, и решил позволить компилятору оптимизировать его.


У меня Google в течение нескольких дней просматривал страницы с описанием таких алгоритмов, как Умножение Карацубы , деление с высоким основанием и Деление Ньютона-Рапсона , но математические символы - это слишком далеко над моей головой. Я бы хотел использовать некоторые из этих продвинутых методов для ускорения моего кода, но сначала мне пришлось бы перевести «греческий» во что-то понятное.

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

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


РЕДАКТИРОВАТЬ: Умножение улучшений

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

Updated raw times:
1/60sec          31080833u
Addition:              ~8u
Subtraction:           ~8u
Multiplication:      ~100u (lowest ~86u, highest around ~256u)
Division:           ~4760u (with maximum bit count)

Вот несколько простых кодов для вас (обратите внимание, что мои имена типов на самом деле разные, это было отредактировано для простоты):

//File: "int128_t.h"
class int128_t
{
    uint32_t dw3, dw2, dw1, dw0;

    // Various constrctors, operators, etc...

    int128_t& operator*=(const int128_t&  rhs) __attribute__((always_inline))
    {
        int128_t Urhs(rhs);
        uint32_t lhs_xor_mask = (int32_t(dw3) >> 31);
        uint32_t rhs_xor_mask = (int32_t(Urhs.dw3) >> 31);
        uint32_t result_xor_mask = (lhs_xor_mask ^ rhs_xor_mask);
        dw0 ^= lhs_xor_mask;
        dw1 ^= lhs_xor_mask;
        dw2 ^= lhs_xor_mask;
        dw3 ^= lhs_xor_mask;
        Urhs.dw0 ^= rhs_xor_mask;
        Urhs.dw1 ^= rhs_xor_mask;
        Urhs.dw2 ^= rhs_xor_mask;
        Urhs.dw3 ^= rhs_xor_mask;
        *this += (lhs_xor_mask & 1);
        Urhs += (rhs_xor_mask & 1);

        struct mul128_t
        {
            int128_t dqw1, dqw0;
            mul128_t(const int128_t& dqw1, const int128_t& dqw0): dqw1(dqw1), dqw0(dqw0){}
        };

        mul128_t data(Urhs,*this);
        asm volatile(
        "push      %%ebp                            \n\
        movl       %%eax,   %%ebp                   \n\
        movl       $0x00,   %%ebx                   \n\
        movl       $0x00,   %%ecx                   \n\
        movl       $0x00,   %%esi                   \n\
        movl       $0x00,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%ebx                   \n\
        adcl       %%edx,   %%ecx                   \n\
        adcl       $0x00,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   24(%%ebp),   %%eax #Calc: (dw1*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%ecx                   \n\
        adcl       %%edx,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   20(%%ebp),   %%eax #Calc: (dw2*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   16(%%ebp),   %%eax #Calc: (dw3*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%ecx                   \n\
        adcl       %%edx,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   24(%%ebp),   %%eax #Calc: (dw1*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   20(%%ebp),   %%eax #Calc: (dw2*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw2)  \n\
        mull              4(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   24(%%ebp),  %%eax #Calc: (dw1*dw2)   \n\
        mull              4(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw3)  \n\
        mull               (%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        pop        %%ebp                            \n"
        :"=b"(this->dw0),"=c"(this->dw1),"=S"(this->dw2),"=D"(this->dw3)
        :"a"(&data):"%ebp");

        dw0 ^= result_xor_mask;
        dw1 ^= result_xor_mask;
        dw2 ^= result_xor_mask;
        dw3 ^= result_xor_mask;
        return (*this += (result_xor_mask & 1));
    }
};

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

Ответы [ 2 ]

2 голосов
/ 08 января 2012

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

Одно из предложений, которое у меня есть, - попытаться использовать наименьшие блоки встроенной сборки, а не кодировать свою логику в сборке. Вы можете написать функцию:

struct div_result { u_int x[2]; };
static inline void mul_add(int a, int b, struct div_result *res);

Функция будет реализована во встроенной сборке, и вы будете вызывать ее из кода C ++. Он должен быть таким же эффективным, как чистая сборка, и гораздо проще кодировать.

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

1 голос
/ 17 января 2012

Правильно ли я понимаю ваши данные о том, что вы выполняете тест на машине с частотой 1,8 ГГц, а "u" в ваших временных параметрах - это такты процессора?

Если так, то 546 циклов для 10 32-битных MUL кажутся мне немного медленными. У меня есть собственный бренд Bignums здесь на Core2 Duo 2 ГГц, и MUL 128x128 = 256 бит работает примерно в 150 циклах (я делаю все 16 маленьких MUL), то есть примерно в 6 раз быстрее. Но это может быть просто более быстрый процессор.

Убедитесь, что вы развернули петли, чтобы сохранить эти накладные расходы. Сохраняйте регистр как можно меньше. Может быть, это поможет, если вы разместите здесь код ASM, чтобы мы могли просмотреть его.

Карацуба вам не поможет, так как он начинает действовать только после 20-40 32-битных слов.

Деление всегда намного дороже, чем умножение. Если вы делите на константу или на одно и то же значение много раз, это может помочь предварительно вычислить обратное значение и затем умножить на него.

...