Вычислить произведение двойного слова (подписанное) из двух слов, учитывая нижнее словосочетание

В восторге от хакера есть алгоритм для вычисления двухсловного произведения двух (подписанных) слов.

Функция muldws1 использует четыре умножения и пять дополнений для вычисления двойное слово из двух слов.

В конце этого кода есть строка, прокомментированная

/* w[1] = u*v;                  // Alternative. */

В этой альтернативе используются пять умножений и четыре сложения, т.е. они обмениваются добавлением для умножения.

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

void muldws1_improved(int w[], int32_t x, int32_t y) {
    uint16_t xl = x; int16_t xh = x >> 16;
    uint16_t yl = y; int16_t yh = y >> 16;

    uint32 lo = x*y;
    int32_t t = xl*yh + xh*yl;

    uint16_t tl = t; int16_t th = t >>16;
    uint16_t loh = lo >> 16;

    int32_t cy = loh<tl; //carry
    int32_t hi = xh*yh + th + cy;
    w[0] = hi; w[1] = lo;
}

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

Можно ли это улучшить? Есть ли лучший способ определить флаг переноса?. Я должен указать, что я также предполагаю, что на оборудовании нет флага переноса (например, нет инструкции ADDC), но слова можно сравнить (например, word1<word).

Изменить: как сказал Sander De Dycker, моя функция терпит неудачу в модульных тестах. Вот версия, которая проходит модульные тесты, но менее эффективна. Я думаю, что это можно улучшить.

void muldws1_improved_v2(int w[], int32_t x, int32_t y) {
    uint16_t xl = x; int16_t xh = x >> 16;
    uint16_t yl = y; int16_t yh = y >> 16;

    uint32_t lo = x*y;
    int32_t  t2 = xl*yh;
    int32_t  t3 = xh*yl;
    int32_t  t4 = xh*yh;

    uint16_t t2l = t2; int16_t t2h = t2 >>16;
    uint16_t t3l = t3; int16_t t3h = t3 >>16;
    uint16_t loh = lo >> 16;

    uint16_t t = t2l + t3l;
    int32_t carry = (t<t2l) + (loh<t);
    int32_t hi = t4 + t2h + t3h + carry;
    w[0] = hi; w[1] = lo;
}

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

Ответ 1

В моем вопросе были две проблемы с моей функцией muldws1_improved. Один из них заключается в том, что он пропустил перенос, когда я сделал xl*yh + xh*yl. Вот почему он не прошел единичные тесты. Но другое заключается в том, что есть подписанные * неподписанные продукты, для которых требуется гораздо больше машинной логики, чем это видно в коде C. (см. Мое правление ниже). Я нашел лучшее решение, которое сначала оптимизировало функцию unsigned product muldwu1, а затем сделать

muldwu1(w,x,y);
w[0] -= ((x<0) ? y : 0)  + ((y<0) ? x : 0);

чтобы исправить знак.

Вот моя попытка улучшить muldwu1, используя нижнее слово lo = x*y (да, эта функция проходит модульные тесты от восторга хакеров).

void muldwu1_improved(uint32_t w[], uint32_t x, uint32_t y) {
    uint16_t xl = x; uint16_t xh = x >> 16;
    uint16_t yl = y; uint16_t yh = y >> 16;

    uint32_t lo   = x*y;    //32x32 to 32
    uint32_t t1   = xl*yh;  //16x16 to 32
    uint32_t t2   = xh*yl;  //16x16 to 32
    uint32_t t3   = xh*yh;  //16x16 to 32

    uint32_t t    = t1 + t2;
    uint32_t tl   = 0xFFFF & t;
    uint32_t th   = t >> 16;
    uint32_t loh  = lo >> 16;

    uint32_t cy   = ((t<t1) << 16) + (loh<tl); //carry
             w[1] = lo;
             w[0] = t3 + th + cy;
}

Это использует одно меньшее дополнение, чем оригинальная функция от восторга хакеров, но он должен делать два сравнения

 1 mul32x32 to 32
 3 mul16x16 to 32
 4 add32
 5 shift logical (or shuffles)
 1 and
 2 compare32
***********
16 operations

Edit:

Меня беспокоило выражение в Hacker Delight (2-е издание), в котором говорится о алгоритмах mulhs и mulhu.

Алгоритм требует 16 основных команд RISC либо в подписанной, либо без знаковой версии, четыре из которых являются умножениями.

Я реализовал беззнаковый алгоритм в только 16 инструкциях SSE, но моя подписанная версия потребовала дополнительных инструкций. Я понял, почему, и теперь я могу ответить на свой вопрос.

Причина, по которой я не смог найти лучшую версию, которая в Hacker Delight заключается в том, что их гипотетический RISC-процессор имеет инструкцию, которая вычисляет нижнее слово произведения двух слов. Другими словами, их алгоритм уже оптимизирован для этого случая, и поэтому вряд ли существует лучшая версия, чем та, которая у них уже есть.

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

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

Почему это имеет значение? В своем алгоритме они делают сначала

u0 = u >> 16;

и позже

t = u0*v1 + k;

если u = 0x80000000 u0 = 0xffff8000. Однако, если ваш процессор может принимать только половинные слова для получения полного слова, верхняя половина слова u0 игнорируется, и вы получаете неправильный подписанный результат.

В этом случае вы должны вычислить неподписанное верхнее слово, а затем исправить с помощью hi -= ((x<0) ? y : 0) + ((y<0) ? x : 0);, как я уже сказал.

Причина, по которой меня это интересует, заключается в том, что команда Intel SIMD (SSE2 через AVX2) не имеет инструкции, которая делает 64x64 до 64, у них только 32x32 до 64. Поэтому моя подписанная версия требует больше инструкций.

Но AVX512 имеет инструкцию 64x64 - 64. Поэтому с AVX512 подписанная версия должна принимать такое же количество инструкций, как и без знака. Однако, поскольку команда 64x64 - 64 может быть намного медленнее, чем инструкция 32x32 - 64, все равно имеет смысл делать неподписанную версию в любом случае, а затем исправлять.