кратчайший способ найти абсолютный минимум. из двух чисел и умножить его на знаки его входных данных в AVX

#simd #avx #avx2

#simd #avx #avx2

Вопрос:

Любой намек на то, как реализовать AVX для логики ниже C без умножения,

 for(int i = 0;i<4096;i  )
{
   out[i] = sign(inp1[i])*sign(inp2[i])*min(abs(inp1[i]), abs(inp2[i])); 
}
  

// inp1, inp2 и out являются 16-битными регистрами.

Ответ №1:

Существует довольно короткое (но неочевидное) решение вашей проблемы:

 res = max(min(a,b), -max(a,b));
  

(Все операции min / max подписаны)

Чтобы объяснить, почему это работает, сначала давайте установим

 A = min(a,b); B = max(a,b);
  

Это по существу сортирует a и b (и исключает случай, A>0 amp;amp; B<0 когда). Теперь нам просто нужно различать 3 случая:

 A<0  amp;amp; B<0:     res = -B 
A<0  amp;amp; B>=0:    res = -min(-A, B) = max(A, -B)
A>=0 amp;amp; B>=0:    res = A
  

К счастью, первый и последний случай также могут быть вычислены как max(A,-B) , поскольку в первом случае A < 0 < -B и в последнем случае -B <= 0 <= A .

В качестве альтернативы, вы могли бы просто спросить (и доверять) WolframAlpha. (не очень полезно, так как оно оценивается только как true «при условии, что a и b положительны» — хотя вы могли бы отобразить разницу между обоими выражениями)


Реализация этого с помощью AVX2 (игнорирование загрузки и сохранения):

 __m256i A = _mm256_min_epi16(a,b);
__m256i B = _mm256_max_epi16(a,b);
__m256i res = _mm256_max_epi16(A, _mm256_sub_epi16(_mm256_setzero_si256(), B));
  

setzero Операция будет выполняться вне любого цикла, поэтому для каждого пакета существует три операции min / max и одна операция psub. На процессорах Intel первый выполняется на портах p01 , а psub выполняется на любом p015 , поэтому цикл будет p01 замкнутым, требуя 1,5 цикла на пакет.

Как отметил @Soonts, -B операция может переполниться, для B=-0x8000 (нет положительного 0x8000 значения для подписанного int16). Это происходит только для a=b=-0x8000 . Если вы предпочитаете выводить 0x7fff в этом случае, вы можете заменить вычитание на насыщенное вычитание ( _mm256_subs_epi16 ).

Комментарии:

1. Хороший; это значительно лучше, чем моя идея, OP должен принять этот ответ.

2. Хороший ответ, но в нем есть ошибка, абсолютный и унарный минус могут переполняться для целых чисел. К счастью, это легко исправить здесь, и исправление бесплатное с точки зрения производительности, замените _mm256_sub_epi16 инструкцию на _mm256_subs_epi16 .

3. @Soonts Я думаю, что проблема возникнет только для a==b==-0x8000 (в этом случае он будет выводить 0x8000 , (где 0x8000 имеется в виду, но, конечно, для целых чисел со знаком это будет рассматриваться как отрицательное). Я добавлю к этому замечание.

Ответ №2:

sign(inp1[i])*sign(inp2[i]) Часть может быть почти точно реализована с _mm256_sign_epi16(in1, in2) помощью, и используя это как 2-й операнд к другому vpsignw , чтобы применить знак этого к min(abs,abs) результату.

psignw отрицает или обнуляет первый операнд, в зависимости от того, является ли 2-й операнд отрицательным или нулевым.(Руководство по встроенным функциям). (Нам не нужна обнуляющая часть psignw : если любой из входных данных равен нулю, беззнаковое минимальное значение их абсолютных значений будет равно нулю. Но мы должны избегать этого в зависимости от того, как мы генерируем входные данные, если это может произойти, когда ни один из наших реальных входных данных не равен нулю.)

Есть угловой случай, когда это неправильно: in1 = INT16_MIN = 0x8000, in2<0 . Результат отрицания in1 все равно будет отрицательным; благодаря тому, что 2 дополняют наибольшее отрицательное число, не имеющее обратного.

Если одно из 2 значений не может быть 0x8000 , используйте это как 1-й аргумент _mm256_sign_epi16 , не требующий дополнительных операций.

@chtz предлагает обходную стратегию: XOR входные данные вместе, чтобы получить правильное значение для знакового бита. Но это вызовет vpsignw поведение обнуления для in1== in2, потому что in1 ^ in2 == 0 . Вы могли or set1(1) бы использовать результат XOR, чтобы убедиться, что он не равен нулю.

 // pseudocode because the full intrinsic names are long and hard to read / type
    sign = (in1 ^ in2) | 1;
    out = psignw( min(abs1,abs2), sign);
  // operation count: XOR, OR, PSIGNW = 3 plus min(abs,abs)
  

В Skylake vpsignw может выполняться на исполнительных портах p0 или p1. Логические значения нравятся vpxor и vpor могут выполняться на любом из p0, p1 или p5. (https://uops.info /) Так что этот способ потенциально лучше, чем другая идея, которая используется psignw дважды. Он «связывает» вместе цепочки зависимостей обоих операндов ранее, на 1 инструкцию, но, вероятно, это будет ограниченная пропускная способность, даже если данные поступают из другой операции за тот же проход.

pabsw и pminuw оба также нуждаются в p0 / p1, не могут выполняться на p5, поэтому выбор одинакового количества инструкций, но использование тех, которые могут использовать порт 5, приводит к лучшему балансу нагрузки на порт выполнения для серверной части Skylake. Zen2 несколько похож, с логическими значениями, способными выполняться на любом порту выполнения FP (0/1/2/3), но psignw / pabsw только FP0 / FP3 и pminuw только FP0 / 1 / 3.


Другой вариант — psignw полностью избежать вместо того, чтобы обходить его поведение при обнулении: XOR, а затем передать знаковый бит с арифметическим сдвигом вправо, затем реализовать условное отрицание с идентификатором дополнения 2 -x = ~x - (-1) . Но это стоит еще одной операции.

     sign = (in1 ^ in2) >> 15;   // pxor  psraw
    out =  (min(abs1,abs2) ^ sign) - sign;  // pxor, psubw
  // operation count: XOR, shift, XOR, SUB = 4 plus min(abs,abs)
  

Другая идея обходного пути заключалась _mm256_or_si256(in1, _mm256_set1_epi16(1)) в том, vpsignw чтобы убедиться, что значение имеет тот же знак, но это не INT16_MIN так.

 // not as good as 
   sign = psignw(in1 | 1, in2);   // VPOR, VPSIGNW
   out = psignw( min(abs1,abs2), sign);
// operation count: OR, 2x PSIGNW = 3 plus min(abs,abs)
  

Арифметический сдвиг вправо на 1 был бы небезопасным: он мог бы сделать операнд нулевым при вводе 1 , что привело бы к окончательному выводу нуля для ввода 1, 2


IDK, если есть какой-нибудь хитрый трюк, который был бы лучше, чем vpabsw на каждый вход отдельно для подачи vpminuw

Комментарии:

1. sign(inp1[i])*sign(inp2[i]) также может быть вычислено с использованием _mm256_xor_si256(inp1[i],inp2[i]) , поскольку имеет значение только верхний бит (также будет использоваться p5 для большинства (всех?) Процессоры Intel). Я думаю, что нашел альтернативный способ, который принимает 3p01 3p015 (вместо 4p01 1p015 ) — это было бы полезно только при смешивании с вашим решением (хотя его нужно проверить).

2. На самом деле _mm256_sign_epi16(in1, in2) неверно (или не соответствует предполагаемому результату), если in1<0 и in2 = -0x8000 .

3. @chtz: целое число VPXOR может работать на любом порту процессоров Intel. Вы думаете vxorps о. Я подумал об этом, но беспокоился, что это может создать a 0 , когда оба входа одинаковы, поэтому следующий vpsignw результат будет равен нулю. например, с in1=in2 = что угодно. Мы могли бы обойти это, введя ненулевой младший бит в результат XOR.

4. Действительно, это проблема (но и в крайнем случае с -0x8000 ). Я запишу свое альтернативное решение (я почти уверен, что оно работает).

5. Я просчитал использование порта в своей первой идее, но на самом деле нашел гораздо более простое решение. Это просто 3p01 1p015 .