Как проверить переполнение для умножения 16-битных целых чисел в SSE? - PullRequest
0 голосов
/ 01 октября 2018

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

// initialized like following:
short I = 0x1BAD; // current injected to neuron
short vR = 0xF00D; // some reset threshold when spiked (negative)

// step to be vectorized:
short v0 = vReset;
for(;;) {

    // v0*v0/16 likely overflows => use 32 bit (16.16)
    short v0_sqr = ((int)v0)*((int)v0) / (1<<(8+4)); // not sure how "(v0*v0)>>(8+4)" would affect sign..
     // or   ((int)v0)*((int)v0) >> (8+4); // arithmetic right shift
     // original paper used v' = (v0^2)/25 + ...

    short v1 = v0_sqr + v0 + I;
    int m; // mask is set when neuron fires
    if(v1_overflows_during_this_operation()) { // "v1 > 0x7FFF" - way to detect?
        m=0xFFFFFFFF;
    else
        m=0;
    v0 = ( v1 & ~m ) | (vR & m );
}

Но у меня нетt нашел инструкцию _mm_mul_epi16(), чтобы проверить старшее слово умножения.Почему и как такая задача v1_overflows_during_this_operation() должна быть реализована в SSE?

1 Ответ

0 голосов
/ 02 октября 2018

В отличие от 32x32 => 64, здесь нет инструкции умножения SSE с расширением 16x16 -> 32.

Вместо этого есть _mm_mulhi_epi16 и _mm_mulhi_epu16, которые дают только подписанные илибеззнаковая верхняя половина полного результата.

_mm_mullo_epi16, что делает упакованное 16x16 => 16-битовое умножение на половину усечения с половиной, то же самое для знака или без знака).

Вы можете использовать _mm_unpacklo/hi_epi16 для чередования половинок низких / высоких частот в пару векторов с 32-битными элементами, но это будет довольно медленно.Но да, вы можете _mm_srai_epi32(v, 8+4) арифметически сдвинуть вправо на 12, а затем переупаковать, возможно, с помощью _mm_packs_epi32 (насыщение со знаком обратно в 16-бит).Тогда я думаю, проверить на насыщенность?


Ваш случай использования необычен.Есть _mm_mulhrs_epi16, который дает старшие 17 бит, округленные и затем усеченные до 16 бит.(См. Описание).Это полезно для некоторых алгоритмов с фиксированной запятой, где входы масштабируются, чтобы поместить результат в верхнюю половину, и вы хотите округлить, включая младшую половину вместо усечения.

Вы могли бы фактически использовать _mm_mulhrs_epi16 или _mm_mulhi_epi16 как лучшая ставка для сохранения максимальной точности, может быть, сдвинув v0 влево перед тем, как возвести в квадрат до той точки, где верхняя половина даст вам (v0*v0) >> (8+4).

Как вы думаете,проще не допустить переполнения результата, а просто создать маску с _mm_cmpge_epi16(v1, vThreshold), как это делает автор в оригинальной статье?

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

С результатом сравнения v0 = ( v1 & ~m ) | (vR & m ); становится смесью SSE4.1: _mm_blendv_epi8.


Есливаш vThreshold имеет 2 неустановленных бита вверху, у вас есть место для сдвига влево, не теряя ни одного из наиболее значимых битов .Поскольку mulhi дает вам (v0*v0) >> 16, то вы можете сделать это:

// losing the high 2 bits of v0
__m128i v0_lshift2   = _mm_slli_epi16(v0, 2);    // left by 2 before squaring
__m128i v0_sqr_asr12 = _mm_mulhi_epi16(v0_lshift2, v0_lshift2);
__m128i v1 = _mm_add_epi16(v0, I);
        v1 = _mm_add_epi16(v1, v0_sqr_asr12);

    // v1 = ((v0<<2)* (int)(v0<<2))) >> 16) + v0 + I

    // v1 = ((v0*(int)v0) >> 12) + v0 + I

Сдвиг влево на 2 до возведения в квадрат такой же, как сдвиг влево на 4 после возведения в квадрат (из полного 32-разрядного результата),Он помещает 16 бит, которые мы хотим, в верхние 16 точно.

Но это непригодно, если ваш v0 настолько близок к полному диапазону, что вы могли бы переполниться при сдвиге влево.

В противном случае вы можете потерять 6 младших битов v0 перед умножением

Округление до -Infinity с арифметическим сдвигом вправо теряет 6 бит точности, но переполнение невозможно.

// losing the low 6 bits of v0
__m128i v0_asr6 = _mm_srai_epi16(v0, 6);
__m128i v0_sqr_asr12 = _mm_mullo_epi16(v0_asr6, v0_asr6);
__m128i v1 = _mm_add_epi16(v0, I);
        v1 = _mm_add_epi16(v1, v0_sqr_asr12);

    // v1 =  (v0>>6) * (int)(v0>>6)) + v0 + I

    // v1 ~= ((v0*(int)v0) >> 12) + v0 + I

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

pmulhrsw округлять вместо усечения может быть даже лучше, если мы можем настроить его так же эффективно.Но я не думаю, что мы можем, потому что сдвиг вправо на 1 является нечетным числом.Я думаю, что нам нужно было бы сделать 2 отдельных входа, один v0_lshift2 и один только влево, смещенный на 1.

...