#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
о. Я подумал об этом, но беспокоился, что это может создать a0
, когда оба входа одинаковы, поэтому следующийvpsignw
результат будет равен нулю. например, с in1=in2 = что угодно. Мы могли бы обойти это, введя ненулевой младший бит в результат XOR.4. Действительно, это проблема (но и в крайнем случае с
-0x8000
). Я запишу свое альтернативное решение (я почти уверен, что оно работает).5. Я просчитал использование порта в своей первой идее, но на самом деле нашел гораздо более простое решение. Это просто
3p01 1p015
.