Clang уже автоматически векторизует это в значительной степени так, как Сунтс предлагал делать вручную. Используйте __restrict
на ваших указателях, поэтому ему не нужна резервная версия, которая работает для перекрытия между некоторыми массивами. Он по-прежнему автоматически векторизован, но он расширяет функцию.
К сожалению, gcc только автоматически векторизуется с -ffast-math
. Я не уверен, какая часть строгого FP останавливает это. (Обновление: требуется только -fno-trapping-math
: это, вероятно, безопасно в большинстве случаев, если вы не снимаете маску с каких-либо исключений FP или не смотрите на флаги исключений MXCSR sticky FP.)
С этой опцией GCC также будет использовать (v)pblendvpd
с -march=nehalem
или -march=znver1
. Посмотри на Годболт
Кроме того, ваша функция C не работает. th
и drop
являются скалярными двойными, но вы объявляете их как const double *
AVX512F позволит вам сравнить !(right[i] >= thresh)
и использовать полученную маску для вычитания с маскированием слиянием.
Элементы, где предикат был истинным, получат left[i] - drop
, другие элементы сохранят свое значение left[i]
, потому что вы объединяете информацию в вектор left
значений.
К сожалению, GCC с -march=skylake-avx512
использует обычный vsubpd
, а затем отдельный vmovapd zmm2{k1}, zmm5
для смешивания, что, очевидно, является пропущенной оптимизацией. Назначение наложения уже является одним из входов SUB.
Использование AVX512VL для 256-битных векторов (в случае, если остальная часть вашей программы не может эффективно использовать 512-битный, поэтому вы не страдаете от снижения турбо тактовой частоты):
__m256d left = ...;
__m256d right = ...;
__mmask8 cmp = _mm256_cmp_pd_mask(right, set1(th), _CMP_NGE_UQ);
__m256d res = _mm256_mask_sub_pd (left, cmp, left, set1(drop));
Итак (помимо загрузки и хранения) это 2 инструкции с AVX512F / VL.
Если вам не нужно определенное поведение NaN в вашей версии, GCC также может автоматически векторизовать
И это более эффективно для всех компиляторов, потому что вам просто нужно AND, а не переменное смешение. Так что это значительно лучше только с SSE2, а также с большинством процессоров, даже если они поддерживают SSE4. 1 blendvpd
, потому что эта инструкция не так эффективна.
Вы можете вычесть 0.0
или drop
из left[i]
на основе результата сравнения.
Создание 0.0
или константы на основе результата сравнения чрезвычайно эффективно: просто инструкция andps
. (Битовый шаблон для 0.0
- это все нули, и SIMD сравнивает производящие векторы со всеми 1 или 0 битами. Таким образом, И сохраняет старое значение или обнуляет его.)
Мы также можем добавить -drop
вместо вычитания drop
. Это требует дополнительного отрицания при вводе, но с AVX позволяет операнду источника памяти для vaddpd
. GCC предпочитает использовать режим индексированной адресации, чтобы на самом деле не уменьшить количество внешних операций на процессорах Intel; это будет "не ламинировать". Но даже с -ffast-math
gcc не выполняет эту оптимизацию самостоятельно, чтобы разрешить сворачивание нагрузки. (Хотя не стоит делать отдельные приращения указателя, если мы не развернем цикл.)
void func3(const double *__restrict left, const double *__restrict right, double *__restrict res,
const size_t size, const double th, const double drop)
{
for (size_t i = 0; i < size; ++i) {
double add = right[i] >= th ? 0.0 : -drop;
res[i] = left[i] + add;
}
}
Внутренний цикл
GCC 9.1 (без каких-либо опций -march
и без -ffast-math
) из ссылки Godbolt выше:
# func3 main loop
# gcc -O3 -march=skylake (without fast-math)
.L33:
vcmplepd ymm2, ymm4, YMMWORD PTR [rsi+rax]
vandnpd ymm2, ymm2, ymm3
vaddpd ymm2, ymm2, YMMWORD PTR [rdi+rax]
vmovupd YMMWORD PTR [rdx+rax], ymm2
add rax, 32
cmp r8, rax
jne .L33
Или простая версия SSE2 имеет внутренний цикл, который в основном такой же, как с left - zero_or_drop
вместо left + zero_or_minus_drop
, поэтому, если вы не можете пообещать компилятору 16-байтовое выравнивание или вы делаете версию AVX, отрицая drop
это просто дополнительные накладные расходы.
Отрицание drop
берет константу из памяти (для XOR знакового бита), и это единственная статическая константа, которая нужна этой функции , так что компромисс стоит рассмотреть в вашем случае, когда цикл не ' Я бегаю огромное количество раз. (Если th
или drop
также не являются константами времени компиляции после вставки и все равно загружаются. Или особенно если -drop
можно вычислить во время компиляции. Или если вы можете заставить вашу программу работать с отрицательным drop
.)
Другое различие между сложением и вычитанием состоит в том, что вычитание не уничтожает знак нуля. -0.0 - 0.0 = -0.0
, +0.0 - 0.0 = +0.0
. В случае, если это имеет значение.
# gcc9.1 -O3
.L26:
movupd xmm5, XMMWORD PTR [rsi+rax]
movapd xmm2, xmm4 # duplicate th
movupd xmm6, XMMWORD PTR [rdi+rax]
cmplepd xmm2, xmm5 # destroy the copy of th
andnpd xmm2, xmm3 # _mm_andnot_pd
addpd xmm2, xmm6 # _mm_add_pd
movups XMMWORD PTR [rdx+rax], xmm2
add rax, 16
cmp r8, rax
jne .L26
GCC использует невыровненные нагрузки, поэтому (без AVX) он не может сложить операнд источника памяти в cmppd
или subpd