Вот некоторые версии, настроенные на основе руководства по микроархам Agner Fog руководство по микроархам и таблицы инструкций. Смотрите также x86 тег вики. Они должны быть эффективными на любом процессоре, без каких-либо серьезных узких мест. (например, я избегал вещей, которые могли бы немного помочь одному уарху, но были бы медленными на другом уарше). Размер кода также минимизирован.
Общая идиома 2x hadd
хороша только для размера кода, а не для скорости на любых существующих процессорах. Для этого есть варианты использования (см. Ниже), но это не один из них.
Я также включил версию AVX. Любой вид горизонтального сокращения с AVX / AVX2 должен начинаться с vextractf128
и «вертикальной» операции, чтобы уменьшить до одного вектора XMM (__m128
).
См. Вывод asm из всего этого кода в проводнике компилятора Godbolt . См. Также мои улучшения в Библиотеке векторных классов Агнера Фога C ++ horizontal_add
функций , ( нить доски сообщений и код на github ). Я использовал макросы CPP для выбора оптимальных тасов для размера кода для SSE2, SSE4 и AVX, а также для избежания movdqa
, когда AVX недоступен.
Есть компромиссы для рассмотрения:
- размер кода: чем меньше, тем лучше по причинам I-кэша L1 и для выборки кода с диска (меньшие двоичные файлы). Общий размер двоичного файла в основном имеет значение для решений компилятора, принимаемых неоднократно по всей программе. Если вы потрудитесь написать что-то вручную с помощью встроенных функций, стоит потратить несколько байтов кода, если это даст какое-либо ускорение для всей программы (будьте осторожны с микробенчмарками, которые делают развертывание хорошо выглядящим).
- Размер uop-кэша: Часто более ценный, чем L1 I $. 4 инструкции по одной операции могут занимать меньше места, чем 2
haddps
, поэтому это очень важно здесь.
- задержка: иногда актуально
- пропускная способность: обычно не имеет значения, горизонтальные суммы не должны быть в самой внутренней петле.
- total uops fused-domain: Если окружающий код не является узким местом на том же порту, который использует hsum, это прокси для влияния hsum на пропускную способность всего этого.
Когда горизонтальное добавление нечасто :
ЦП без uop-кэша может предпочесть 2x haddps
: когда он работает, он работает медленнее, но это не часто. Только две инструкции сводят к минимуму влияние на окружающий код (размер I $).
ЦП с кешем uop , вероятно, предпочтут что-то, что займет меньше мопов, даже если это больше инструкций / больше размер кода x86. Общее количество используемых строк кэша мопов - это то, что мы хотим минимизировать, что не так просто, как минимизация общего количества мопов (взятые ветви и границы 32B всегда начинают новую строку кэша мопов).
В любом случае, с учетом вышесказанного, горизонтальные суммы составляют лот , поэтому я попытаюсь тщательно составить несколько версий, которые хорошо компилируются. Не тестируется на каком-либо реальном оборудовании или даже тщательно проверяется. Могут быть ошибки в константах тасования или что-то в этом роде.
Если вы делаете резервную или базовую версию своего кода, помните, что только старые процессоры будут запускать его ; на более новых процессорах будет работать ваша версия AVX, или SSE4.1, или что-то еще.
Старые процессоры, такие как K8 и Core2 (merom) и более ранние, имеют только 64-битные тасовые блоки . Core2 имеет 128-битные исполнительные блоки для большинства команд, но не для случайных. (Pentium M и K8 обрабатывают все 128-битные векторные инструкции как две 64-битные половины).
Тасования, подобные movhlps
, которые перемещают данные в 64-битных чанках (без тасования в 64-битных половинах), также бывают быстрыми.
На старых процессорах с медленным перемешиванием :
movhlps
(Merom: 1uop) значительно быстрее, чем shufps
(Merom: 3uops). На Пентиуме-М дешевле movaps
. Кроме того, он работает в домене FP на Core2, избегая задержек обхода из-за других перемешиваний.
unpcklpd
быстрее, чем unpcklps
.
pshufd
медленно, pshuflw
/ pshufhw
быстро (потому что они тасуют только 64-битную половину) pshufb mm0
(MMX) быстро, pshufb xmm0
медленно.
haddps
очень медленно (6 моп на Merom и Pentium M)
movshdup
(Merom: 1uop) интересно : это единственный insop 1uop, который тасует в элементах 64b.
shufps
на Core2 (включая Penryn) переносит данные в целочисленную область, вызывая задержку обхода, чтобы вернуть их к исполнительным блокам FP для addps
, но movhlps
полностью находится в области FP. shufpd
также работает в домене с плавающей точкой.
movshdup
работает в целочисленной области, но это только один моп.
AMD K10, Intel Core2 (Penryn / Wolfdale) и все последующие процессоры запускают все xmm-тасовки как один моп. (Но обратите внимание на задержку обхода с shufps
на Пенрине, избегаемую с movhlps
)
Без AVX, избегая напрасной потери movaps
/ movdqa
инструкции требуют тщательного выбора шаффлов . Только несколько перемешиваний работают как копирование и перемешивание, а не как изменение места назначения. Перемешивания, которые объединяют данные из двух входов (например, unpck*
или movhlps
), могут использоваться с переменной tmp, которая больше не нужна, вместо _mm_movehl_ps(same,same)
.
Некоторые из них можно сделать быстрее (за исключением MOVAPS), но сделать их более уродливыми или менее «чистыми», взяв фиктивный аргумент для использования в качестве пункта назначения для начального перемешивания. Например:
// Use dummy = a recently-dead variable that vec depends on,
// so it doesn't introduce a false dependency,
// and the compiler probably still has it in a register
__m128d highhalf_pd(__m128d dummy, __m128d vec) {
#ifdef __AVX__
// With 3-operand AVX instructions, don't create an extra dependency on something we don't need anymore.
(void)dummy;
return _mm_unpackhi_pd(vec, vec);
#else
// Without AVX, we can save a MOVAPS with MOVHLPS into a dead register
__m128 tmp = _mm_castpd_ps(dummy);
__m128d high = _mm_castps_pd(_mm_movehl_ps(tmp, _mm_castpd_ps(vec)));
return high;
#endif
}
SSE1 (он же SSE):
float hsum_ps_sse1(__m128 v) { // v = [ D C | B A ]
__m128 shuf = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 3, 0, 1)); // [ C D | A B ]
__m128 sums = _mm_add_ps(v, shuf); // sums = [ D+C C+D | B+A A+B ]
shuf = _mm_movehl_ps(shuf, sums); // [ C D | D+C C+D ] // let the compiler avoid a mov by reusing shuf
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
# gcc 5.3 -O3: looks optimal
movaps xmm1, xmm0 # I think one movaps is unavoidable, unless we have a 2nd register with known-safe floats in the upper 2 elements
shufps xmm1, xmm0, 177
addps xmm0, xmm1
movhlps xmm1, xmm0 # note the reuse of shuf, avoiding a movaps
addss xmm0, xmm1
# clang 3.7.1 -O3:
movaps xmm1, xmm0
shufps xmm1, xmm1, 177
addps xmm1, xmm0
movaps xmm0, xmm1
shufpd xmm0, xmm0, 1
addss xmm0, xmm1
Я сообщил о лягушке о пессимизации шаффлов . Он имеет свое внутреннее представление для тасования и превращает его обратно в тасования gcc чаще использует инструкции, которые непосредственно соответствуют встроенному вами.
Часто clang работает лучше, чем gcc, в коде, где выбор инструкций не настраивается вручную, или постоянное распространение может упростить вещи, даже если внутренние значения оптимальны для непостоянного случая. В целом, хорошо, что компиляторы работают как настоящий компилятор для встроенных функций, а не просто как ассемблер. Компиляторы часто могут генерировать хороший ассм из скалярного C, который даже не пытается работать так, как это делал бы хороший ассемблер. В конечном итоге компиляторы будут воспринимать встроенные функции как еще один оператор Си как входные данные для оптимизатора.
SSE3
float hsum_ps_sse3(__m128 v) {
__m128 shuf = _mm_movehdup_ps(v); // broadcast elements 3,1 to 2,0
__m128 sums = _mm_add_ps(v, shuf);
shuf = _mm_movehl_ps(shuf, sums); // high half -> low half
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
# gcc 5.3 -O3: perfectly optimal code
movshdup xmm1, xmm0
addps xmm0, xmm1
movhlps xmm1, xmm0
addss xmm0, xmm1
Это имеет несколько преимуществ:
не требуется копий movaps
для работы с деструктивными шаффлами (без AVX): пункт назначения movshdup xmm1, xmm2
только для записи, поэтому он создает tmp
из мертвого регистра для нас. По этой же причине я использовал movehl_ps(tmp, sums)
вместо movehl_ps(sums, sums)
.
маленький размер кода. Инструкции перетасовки малы: movhlps
- 3 байта, movshdup
- 4 байта (аналогично shufps
). Прямой байт не требуется, поэтому в AVX vshufps
равен 5 байтов, но vmovhlps
и vmovshdup
равны 4.
Я мог бы сохранить другой байт с помощью addps
вместо addss
. Поскольку это не будет использоваться во внутренних контурах, дополнительная энергия для переключения дополнительных транзисторов, вероятно, незначительна. Исключения FP из верхних 3 элементов не являются риском, потому что все элементы содержат действительные данные FP. Однако clang / LLVM на самом деле «понимает» тасования векторов и генерирует лучший код, если знает, что важен только младший элемент.
Как и в версии SSE1, добавление нечетных элементов к самим себе может вызвать исключения FP (например, переполнение), которые не произошли бы в противном случае, но это не должно быть проблемой. Денормалы медленные, но IIRC, дающий результат + Inf, не на большинстве уарчей.
SSE3, оптимизирующий под размер кода
Если основной проблемой является размер кода, две инструкции haddps
(_mm_hadd_ps
) помогут вам (ответ Пола Р.). Это также самый простой для ввода и запоминания. Это не быстро , хотя. Даже Intel Skylake по-прежнему декодирует каждый haddps
до 3 мопов с задержкой в 6 циклов. Таким образом, несмотря на то, что он сохраняет байты машинного кода (I-кэш L1), он занимает больше места в более ценном uop-кэше. Реальные сценарии использования для haddps
: проблемы транспонирования и суммирования или выполнения некоторого масштабирования на промежуточном этапе в этой реализации SSE atoi()
.
AVX:
Эта версия сохраняет байт кода против Ответ Марата на вопрос AVX .
#ifdef __AVX__
float hsum256_ps_avx(__m256 v) {
__m128 vlow = _mm256_castps256_ps128(v);
__m128 vhigh = _mm256_extractf128_ps(v, 1); // high 128
vlow = _mm_add_ps(vlow, vhigh); // add the low 128
return hsum_ps_sse3(vlow); // and inline the sse3 version, which is optimal for AVX
// (no wasted instructions, and all of them are the 4B minimum)
}
#endif
vmovaps xmm1,xmm0 # huh, what the heck gcc? Just extract to xmm1
vextractf128 xmm0,ymm0,0x1
vaddps xmm0,xmm1,xmm0
vmovshdup xmm1,xmm0
vaddps xmm0,xmm1,xmm0
vmovhlps xmm1,xmm1,xmm0
vaddss xmm0,xmm0,xmm1
vzeroupper
ret
Двойная точность:
double hsum_pd_sse2(__m128d vd) { // v = [ B | A ]
__m128 undef = _mm_undefined_ps(); // don't worry, we only use addSD, never touching the garbage bits with an FP add
__m128 shuftmp= _mm_movehl_ps(undef, _mm_castpd_ps(vd)); // there is no movhlpd
__m128d shuf = _mm_castps_pd(shuftmp);
return _mm_cvtsd_f64(_mm_add_sd(vd, shuf));
}
# gcc 5.3.0 -O3
pxor xmm1, xmm1 # hopefully when inlined, gcc could pick a register it knew wouldn't cause a false dep problem, and avoid the zeroing
movhlps xmm1, xmm0
addsd xmm0, xmm1
# clang 3.7.1 -O3 again doesn't use movhlps:
xorpd xmm2, xmm2 # with #define _mm_undefined_ps _mm_setzero_ps
movapd xmm1, xmm0
unpckhpd xmm1, xmm2
addsd xmm1, xmm0
movapd xmm0, xmm1 # another clang bug: wrong choice of operand order
// This doesn't compile the way it's written
double hsum_pd_scalar_sse2(__m128d vd) {
double tmp;
_mm_storeh_pd(&tmp, vd); // store the high half
double lo = _mm_cvtsd_f64(vd); // cast the low half
return lo+tmp;
}
# gcc 5.3 -O3
haddpd xmm0, xmm0 # Lower latency but less throughput than storing to memory
# ICC13
movhpd QWORD PTR [-8+rsp], xmm0 # only needs the store port, not the shuffle unit
addsd xmm0, QWORD PTR [-8+rsp]
Хранениев память и обратно избегает ALU UOP.Это хорошо, если давление порта перетасовки или ALU Uops в целом являются узким местом.(Обратите внимание, что ему не нужно sub rsp, 8
или что-либо еще, потому что x86-64 SysV ABI предоставляет красную зону, на которую обработчики сигналов не будут наступать.)
Некоторые люди сохраняют в массив и суммируютвсе элементы, но компиляторы обычно не понимают, что младший элемент массива все еще находится в регистре перед хранилищем.
Integer:
pshufd
isудобное копирование и перемешивание.К сожалению, сдвиги битов и байтов на месте, и punpckhqdq
помещает верхнюю половину получателя в нижнюю половину результата, в отличие от movhlps
, который может извлечь верхнюю половину в другой регистр.
Использование movhlps
для первого шага может быть полезно на некоторых процессорах, но только если у нас есть чистая рег.pshufd
- это безопасный выбор, быстрый после всего, что есть после Merom.
int hsum_epi32_sse2(__m128i x) {
#ifdef __AVX__
__m128i hi64 = _mm_unpackhi_epi64(x, x); // 3-operand non-destructive AVX lets us save a byte without needing a mov
#else
__m128i hi64 = _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2));
#endif
__m128i sum64 = _mm_add_epi32(hi64, x);
__m128i hi32 = _mm_shufflelo_epi16(sum64, _MM_SHUFFLE(1, 0, 3, 2)); // Swap the low two elements
__m128i sum32 = _mm_add_epi32(sum64, hi32);
return _mm_cvtsi128_si32(sum32); // SSE2 movd
//return _mm_extract_epi32(hl, 0); // SSE4, even though it compiles to movd instead of a literal pextrd r32,xmm,0
}
# gcc 5.3 -O3
pshufd xmm1,xmm0,0x4e
paddd xmm0,xmm1
pshuflw xmm1,xmm0,0x4e
paddd xmm0,xmm1
movd eax,xmm0
int hsum_epi32_ssse3_slow_smallcode(__m128i x){
x = _mm_hadd_epi32(x, x);
x = _mm_hadd_epi32(x, x);
return _mm_cvtsi128_si32(x);
}
На некоторых процессорах безопасно использовать FP-тасовки для целочисленных данных.Я этого не делал, поскольку на современных процессорах, которые максимально сохраняют 1 или 2 байта кода, без увеличения скорости (кроме размера кода / эффектов выравнивания).