Самый быстрый способ умножить два 64-битных int на 128-битные, а затем ›› на 64-битные?

Мне нужно умножить два 64-битных целых числа со знаком a и b вместе, а затем сдвинуть результат (128-битный) на 64-битное целое число со знаком. Как это сделать быстрее всего?

Мои 64-битные целые числа фактически представляют собой числа с фиксированной запятой с fmt дробными битами. fmt выбирается таким образом, чтобы a * b >> fmt не переполнялся, например, abs(a) < 64<<fmt и abs(b) < 2<<fmt с fmt==56 никогда не будут переполняться в 64-битном формате, так как окончательный результат будет < 128<<fmt и, следовательно, поместится в int64.

Причина, по которой я хочу это сделать, состоит в том, чтобы быстро и точно оценить полиномы пятой степени формы ((((c5*x + c4)*x + c3)*x + c2)*x + c1)*x + c0 в формате с фиксированной точкой, где каждое число представляет собой 64-битное число со знаком с фиксированной точкой и fmt дробными битами. Я ищу наиболее эффективный способ добиться этого.


person Michel Rouzic    schedule 27.07.2015    source источник
comment
Ваша постановка вопроса предполагает, что вы, возможно, уже пробовали реализацию. Если да, можете ли вы опубликовать свой код?   -  person ryyker    schedule 27.07.2015
comment
Я подозреваю, что самый быстрый способ сделать это - просто сделать это (при условии, что у вас есть существующая реализация int128, которой вы можете воспользоваться).   -  person Oliver Charlesworth    schedule 27.07.2015
comment
@ryyker У меня нет, я пробовал то же самое с int32, double и __float128, но никогда с int64, поэтому мне никогда не приходилось иметь дело с результатом int128.   -  person Michel Rouzic    schedule 27.07.2015
comment
@Oliver Charlesworth. Это переносимый код, я не знаю о реализации int128, которая была бы широко доступна. Я подумал, что то, что не требует типа int128, было бы осуществимо, поскольку в конце концов, что бы ни делал компилятор, я могу делать то, что я могу делать без типа int128, верно? Я думаю, что требование сдвига для получения результата int64 может позволить использовать некоторые хитрые трюки.   -  person Michel Rouzic    schedule 27.07.2015
comment
Было бы полезно получить некоторую информацию об ISA. Обычно намного проще написать его непереносимым.   -  person user3528438    schedule 27.07.2015
comment
@ user3528438 хорошо, это вообще современные ПК, в основном x86_64. Я мог бы сделать это непереносимым способом с переносным резервным вариантом, если это необходимо.   -  person Michel Rouzic    schedule 27.07.2015
comment
См. Здесь предложение об использовании SSE4, stackoverflow.com/questions/17863411/   -  person Jens Munk    schedule 27.07.2015


Ответы (1)


Как отметил комментатор по вопросу, это легче всего эффективно выполнить с помощью машинно-зависимого кода, а не переносимого кода. Спрашивающий заявляет, что основная платформа - x86_64, и в ней есть встроенная инструкция для выполнения 64 ✕ 64 → 128-битного умножения. К нему легко получить доступ, используя небольшой кусок встроенной сборки. Обратите внимание, что детали встроенной сборки могут несколько отличаться от компилятора, приведенный ниже код был создан с помощью компилятора Intel C / C ++.

#include <stdint.h>

/* compute mul_wide (a, b) >> s, for s in [0,63] */
int64_t mulshift (int64_t a, int64_t b, int s)
{
    int64_t res;
    __asm__ (
        "movq  %1, %%rax;\n\t"          // rax = a
        "movl  %3, %%ecx;\n\t"          // ecx = s
        "imulq %2;\n\t"                 // rdx:rax = a * b
        "shrdq %%cl, %%rdx, %%rax;\n\t" // rax = int64_t (rdx:rax >> s)
        "movq  %%rax, %0;\n\t"          // res = rax
        : "=rm" (res)
        : "rm"(a), "rm"(b), "rm"(s)
        : "%rax", "%rdx", "%ecx");
    return res;
}

Портативный C99, эквивалентный приведенному выше коду, показан ниже. Я тщательно тестировал это на версии со встроенной сборкой, и никаких несоответствий обнаружено не было.

void umul64wide (uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo)
{
    uint64_t a_lo = (uint64_t)(uint32_t)a;
    uint64_t a_hi = a >> 32;
    uint64_t b_lo = (uint64_t)(uint32_t)b;
    uint64_t b_hi = b >> 32;

    uint64_t p0 = a_lo * b_lo;
    uint64_t p1 = a_lo * b_hi;
    uint64_t p2 = a_hi * b_lo;
    uint64_t p3 = a_hi * b_hi;

    uint32_t cy = (uint32_t)(((p0 >> 32) + (uint32_t)p1 + (uint32_t)p2) >> 32);

    *lo = p0 + (p1 << 32) + (p2 << 32);
    *hi = p3 + (p1 >> 32) + (p2 >> 32) + cy;
}

void mul64wide (int64_t a, int64_t b, int64_t *hi, int64_t *lo)
{
    umul64wide ((uint64_t)a, (uint64_t)b, (uint64_t *)hi, (uint64_t *)lo);
    if (a < 0LL) *hi -= b;
    if (b < 0LL) *hi -= a;
}

/* compute mul_wide (a, b) >> s, for s in [0,63] */
int64_t mulshift (int64_t a, int64_t b, int s)
{
    int64_t res;
    int64_t hi, lo;
    mul64wide (a, b, &hi, &lo);
    if (s) {
        res = ((uint64_t)hi << (64 - s)) | ((uint64_t)lo >> s);
    } else {
        res = lo;
    }
    return res;
}
person njuffa    schedule 27.07.2015
comment
Собирался сделать реализацию, комбинируя множители 32x32-> 64 бит. Не было инструкции imulq. Ваше решение проверенное - оно работает как ожидалось - person Jens Munk; 28.07.2015
comment
Огромное спасибо! Теперь мне просто нужен переносимый запасной вариант (для все еще необходимых 32-битных сборок или, возможно, других платформ), чтобы поддержать его. - person Michel Rouzic; 28.07.2015
comment
Позвольте мне посмотреть, что я могу сделать с точки зрения переносимого резервного кода. Не должно быть слишком сложно. - person njuffa; 29.07.2015
comment
Вместо использования встроенной сборки попробуйте следующее: #include ‹x86intrin.h› uint64_t multophalf_intrinsic (uint64_t a, uint64_t b) {unsigned long long hi = 0; _mulx_u64 (а, б, & привет); ответь привет; } - person jorgbrown; 08.03.2018