Самый быстрый способ вычисления абсолютного значения с помощью SSE

Я знаю 3 метода, но, насколько мне известно, обычно используются только первые 2:

1) Маска с бита знака с использованием andps или andnotps .

  • Плюсы: одна быстрая инструкция, если маска уже находится в регистре, что делает ее идеальной для выполнения этого много раз в цикле.
  • Минусы: Маска не может быть в регистре или хуже, даже в кеше, что вызывает очень долгую выборку памяти.

2) Вычтите значение от нуля до отрицания, а затем получите максимальное значение оригинала и отрицание.

  • Плюсы: фиксированная стоимость, потому что ничего не нужно, чтобы получить, например, маску.
  • Минусы: всегда будет медленнее, чем метод маски, если условия идеальны, и мы должны дождаться завершения subps прежде чем использовать инструкцию maxps .

3) Подобно опции 2, вычтите исходное значение от нуля до отрицания, но затем «побитовое» и результат с оригиналом, использующим andps . Я проверил тест, сравнивая это с методом 2, и он, похоже, ведет себя одинаково с методом 2, кроме случаев, когда имеет дело с NaN s, и в этом случае результатом будет другой NaN чем результат метода 2.

  • Плюсы: должно быть немного быстрее, чем метод 2, потому что andps обычно быстрее, чем maxps .
  • Минусы: может ли это привести к каким-либо непреднамеренным действиям при использовании NaN ? Может быть, нет, потому что NaN все еще NaN , даже если это другое значение NaN , правильно?

Мысли и мнения приветствуются.

TL; DR: почти во всех случаях используйте pcmpeq / shift для генерации маски и / или использовать ее. Он имеет самый короткий критический путь (привязан к постоянной памяти) и не может кэшировать.

Как это сделать с помощью встроенных функций

Получение компилятора для испускания pcmpeqd в неинициализированном регистре может быть сложным. (godbolt) . Лучший способ для gcc / icc выглядит

 __m128 abs_mask(void){ // with clang, this turns into a 16B load, // with every calling function getting its own copy of the mask __m128i minus1 = _mm_set1_epi32(-1); return _mm_castsi128_ps(_mm_srli_epi32(minus1, 1)); } // MSVC is BAD when inlining this into loops __m128 vecabs_and(__m128 v) { return _mm_and_ps(abs_mask(), v); } __m128 sumabs(const __m128 *a) { // quick and dirty no alignment checks __m128 sum = vecabs_and(*a); for (int i=1 ; i < 10000 ; i++) { // gcc, clang, and icc hoist the mask setup out of the loop after inlining // MSVC doesn't! sum = _mm_add_ps(sum, vecabs_and(a[i])); // one accumulator makes addps latency the bottleneck, not throughput } return sum; } 

clang 3.5 и более поздние версии «оптимизирует» set1 / shift для загрузки константы из памяти. Однако он будет использовать pcmpeqd для реализации set1_epi32(-1) . TODO: найдите последовательность свойств, которая создает желаемый машинный код с clang . Загрузка константы из памяти не является катастрофой производительности, но при использовании каждой функции другая копия маски довольно ужасная.

MSVC : VS2013:

  • _mm_uninitialized_si128() не определен.

  • _mm_cmpeq_epi32(self,self) в неинициализированной переменной movdqa xmm, [ebp-10h] в этом тестовом случае (т. е. загружает некоторые неинициализированные данные из стека. Это меньше рискует пропустить кеш, чем просто загружать конечную константу из Однако Kumputer говорит, что MSVC не удалось вытащить pcmpeqd / psrld из цикла (я предполагаю, что при встраивании vecabs ), поэтому это vecabs , если вы вручную не встраиваете и не выталкиваете константу из цикла самостоятельно.

  • Использование _mm_srli_epi32(_mm_set1_epi32(-1), 1) приводит к тому, что movdqa загружает вектор всех -1 (поднятых вне цикла) и psrld внутри цикла. Так что это ужасно. Если вы собираетесь загрузить константу 16B, это должен быть конечный вектор. Имея целые инструкции, генерирующие маску, каждая итерация цикла также ужасна.

Предложения для MSVC: отказаться от создания маски «на лету» и просто написать

 const __m128 absmask = _mm_castsi128_ps(_mm_set1_epi32(~(1<<31)); 

Вероятно, вы просто получите маску, сохраненную в памяти, как постоянную 16B. Надеемся, что не будет дублироваться для каждой функции, которая его использует. Наличие маски в постоянной памяти более вероятно, будет полезно в 32-битном коде, где у вас есть только 8 регистров XMM, поэтому vecabs могут просто ANDPS с операндом источника памяти, если у него нет свободного регистра, чтобы поддерживать постоянную работу ,

TODO: узнайте, как избежать дублирования константы везде, где она встроена. Вероятно, использование глобальной константы, а не анонимного set1 , было бы неплохо. Но тогда вам нужно инициализировать его, но я не уверен, что intrinsics работают как инициализаторы для глобальных переменных __m128 . Вы хотите, чтобы он заходил в раздел данных только для чтения, а не для конструктора, который запускается при запуске программы.


Альтернативно, используйте

 __m128i minus1; // undefined #if _MSC_VER && !__INTEL_COMPILER minus1 = _mm_setzero_si128(); // PXOR is cheaper than MSVC's silly load from the stack #endif minus1 = _mm_cmpeq_epi32(minus1, minus1); // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead. const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1)); 

Дополнительный PXOR довольно дешев, но он по-прежнему равен uop и еще 4 байта по размеру кода. Если у кого-то есть лучшее решение для преодоления нежелания MSVC испускать нужный нам код, оставьте комментарий или отредактируйте. Это нехорошо, если вставить в петлю, хотя, потому что pxor / pcmp / psrl все будет внутри цикла.

Загрузка 32-битной константы с movd и трансляция с помощью shufps может быть в порядке (опять же, вам, вероятно, придется вручную вытащить это из цикла). Это 3 инструкции (mov-немедленный для GP reg, movd, shufps), а movd медленнее на AMD, где векторная единица делится между двумя целыми ядрами. (Их версия гиперstreamа.)


Выбор лучшей последовательности asm

Хорошо, давайте посмотрим на это, скажем, Intel Sandybridge через Skylake, с небольшим упоминанием о Nehalem. Обратитесь к руководствам микроархива Agner Fog и инструкциям по настройке, как я это сделал. Я также использовал номера Skylake, которые были связаны в сообщении на форумах http://realwordtech.com/ .


Допустим, что вектор, который мы хотим использовать abs() находится в xmm0 и является частью длинной цепи зависимостей, что характерно для кода FP.

Поэтому допустим, что любые операции, которые не зависят от xmm0 могут начинаться с нескольких циклов до того, как xmm0 будет готов. Я тестировал, а инструкции с операндами памяти не добавляют лишнюю задержку в цепочку зависимостей, предполагая, что адрес операнда памяти не является частью цепочки dep (т. Е. Не является частью критического пути).


Я не совсем понимаю, как можно начать работу с памятью, когда она входит в состав микро-fused uop. Насколько я понимаю, Buffer Re-Order Buffer (ROB) работает с плавными uops, а треки от выхода до выхода на пенсию (от 168 (SnB) до 224 (SKL)). Существует также планировщик, который работает в незанятом домене, поддерживая только uops, которые имеют свои входные операнды, но еще не выполнены. В то же время, когда они декодируются (или загружаются из кеша uop), uops могут входить в ROB (сплавленный) и планировщик (не используется). Если я правильно понимаю это, это от 54 до 64 записей в Sandybridge до Broadwell и 97 в Skylake. Есть некоторые необоснованные предположения о том, что он больше не является унифицированным (ALU / load-store) планировщиком .

Там также говорят о том, что Skylake обрабатывает 6 часов в час. Насколько я понимаю, Skylake будет считывать целые строки uop-cache (до 6 uops) за такт в буфер между кэшем uop и ROB. Проблема в ROB / scheduler по-прежнему 4-х. (Даже nop все еще 4 за часы). Этот буфер помогает, когда границы линии выравнивания кода / uop кэша вызывают узкие места для предыдущих проектов Sandybridge-microarch. Раньше я думал, что эта «очередь вопросов» была этим буфером, но, видимо, это не так.

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


1a: маска с операндом памяти

 ANDPS xmm0, [mask] # in the loop 
  • байты: 7 insn, 16 данных. (AVX: 8 insn)
  • fused-domain uops: 1 * n
  • латентность добавлена ​​к критическому пути: 1c (при условии, что кэш L1 попал)
  • пропускная способность: 1 / c. (Skylake: 2 / c) (ограничен 2 нагрузками / c)
  • «latency», если xmm0 был готов, когда этот insn выдал: ~ 4c в кеше L1.

1b: маска из регистра

 movaps xmm5, [mask] # outside the loop ANDPS xmm0, xmm5 # in a loop # or PAND xmm0, xmm5 # higher latency, but more throughput on Nehalem to Broadwell # or with an inverted mask, if set1_epi32(0x80000000) is useful for something else in your loop: VANDNPS xmm0, xmm5, xmm0 # It's the dest that's NOTted, so non-AVX would need an extra movaps 
  • байты: 10 insn + 16 данных. (AVX: 12 insn байтов)
  • fused-domain uops: 1 + 1 * n
  • латентность, добавленная к цепочке отбрасывания: 1c (с тем же предостережением о пропуске кеша для начала цикла)
  • пропускная способность: 1 / c. (Skylake: 3 / c)

PAND - пропускная способность 3 / c на Nehalem до Broadwell, но латентность = 3c (если используется между двумя операциями FP-домена и еще хуже на Nehalem). Я предполагаю, что только port5 имеет проводку для пересылки побитовых операций непосредственно на другие исполнительные блоки FP (pre Skylake). Pre-Nehalem и AMD, побитовые операторы FP обрабатываются одинаково с целыми операциями FP, поэтому они могут работать на всех портах, но имеют задержку пересылки.


1c: генерировать маску «на лету»:

 # outside a loop PCMPEQD xmm5, xmm5 # set to 0xff... Recognized as independent of the old value of xmm5, but still takes an execution port (p1/p5). PSRLD xmm5, 1 # 0x7fff... # port0 # or PSLLD xmm5, 31 # 0x8000... to set up for ANDNPS ANDPS xmm0, xmm5 # in the loop. # port5 
  • байтов: 12 (AVX: 13)
  • fused-domain uops: 2 + 1 * n (без операций с памятью)
  • латентность, добавленная к цепочке отбрасывания: 1c
  • пропускная способность: 1 / c. (Skylake: 3 / c)
  • пропускная способность для всех 3 мкп: 1 / c, насыщающая все 3 порта порта ALU
  • «latency», если xmm0 был готов, когда эта последовательность была выпущена (без цикла): 3c (+ 1c) может задержать байпас на SnB / IvB, если ANDPS должен ждать, пока будут получены целые данные. Agner Fog говорит, что в некоторых случаях нет дополнительной задержки для integer-> FP-boolean на SnB / IvB.)

Эта версия по-прежнему занимает меньше памяти, чем версии с постоянной 16B в памяти. Это также идеально подходит для нерегулярно называемой функции, потому что нет никакой нагрузки, чтобы страдать от промаха в кеше.

«Задержка байпаса» не должна быть проблемой. Если xmm0 является частью длинной цепочки зависимостей, команды генерации маски будут выполняться задолго до того, чтобы результат целого числа xmm5 успел дотянуться до ANDPS до того, как xmm0 будет готов, даже если он займет медленную полосу.

По словам тестирования Agner Fog, у Haswell нет байпасной задержки для целочисленных результатов -> FP boolean. Его описание для SnB / IvB говорит, что это имеет место с выводами некоторых целых инструкций. Таким образом, даже в случае «начального начала» начала-от-цепочки, где xmm0 готов, когда эта последовательность команд выдается, это всего лишь 3c на * хорошо, 4c на * Bridge. Задержка, вероятно, не имеет значения, освобождают ли исполнительные блоки отставание от uops так быстро, как они выдаются.

В любом случае выход ANDPS будет в домене FP и не будет иметь байпасной задержки, если используется в MULPS или что-то в MULPS роде.

На Nehalem байпасные задержки составляют 2c. Таким образом, в начале цепочки отбрасывания (например, после неверного предсказания ветвления или промаха I $) на Nehalem, «задержка», если xmm0 был готов, когда эта последовательность была выпущена 5c. Если вы очень много заботитесь о Nehalem и ожидаете, что этот код станет первым, что будет происходить после частых ветвей неправильных outlookов или аналогичных конвейеров, которые не позволят машине OoOE не приступить к вычислению маски до того, как xmm0 будет готов, тогда это может быть не так лучший выбор для ситуаций без цикла.


2a: AVX max (x, 0-x)

 VXORPS xmm5, xmm5, xmm5 # outside the loop VSUBPS xmm1, xmm5, xmm0 # inside the loop VMAXPS xmm0, xmm0, xmm1 
  • байтов: AVX: 12
  • fused-domain uops: 1 + 2 * n (без операций с памятью)
  • латентность, добавленная к цепочке разломов: 6c (Skylake: 8c)
  • пропускная способность: 1 на 2c (два порта 1 uops). (Skylake: 1 / c, предполагая, что MAXPS использует те же два порта, что и SUBPS .)

Skylake сбрасывает отдельный блок добавления вектора FP и добавляет вектор в блоки FMA на портах 0 и 1. Это удваивает пропускную способность FP, за счет увеличения задержки на 1 c. Задержка FMA уменьшается до 4 (от 5 в * хорошо) . x87 FADD по-прежнему занимает 3 цикла задержки, поэтому есть еще 3-цикл скалярный 80bit-FP сумматор, но только на одном порту.

2b: тот же, но без AVX:

 # inside the loop XORPS xmm1, xmm1 # not on the critical path, and doesn't even take an execution unit on SnB and later SUBPS xmm1, xmm0 MAXPS xmm0, xmm1 
  • байт: 9
  • fused-domain uops: 3 * n (без операций с памятью)
  • латентность, добавленная к цепочке разломов: 6c (Skylake: 8c)
  • пропускная способность: 1 на 2c (два порта 1 uops). (Skylake: 1 / c)
  • «latency», если xmm0 был готов, когда эта последовательность была выпущена (без цикла): тот же

Обнуление регистра с xorps same,same которую процессор распознает (например, xorps same,same ) обрабатывается во время переименования регистра в микроархитектуре семейства Sandbridge, имеет нулевую задержку и пропускную способность 4 / c. (То же, что и reg-> reg, что IvyBridge и позже может устранить).

Однако он не является бесплатным: он все еще занимает uop в плавленном домене, поэтому, если ваш код только узкий по скорости 4uop / cycle, это замедлит вас. Это более вероятно при гиперstreamе.


3: ANDPS (x, 0-x)

 VXORPS xmm5, xmm5, xmm5 # outside the loop. Without AVX: zero xmm1 inside the loop VSUBPS xmm1, xmm5, xmm0 # inside the loop VANDPS xmm0, xmm0, xmm1 
  • байтов: AVX: 12 без AVX: 9
  • fused-domain uops: 1 + 2 * n (без операций с памятью). (Без AVX: 3 * n)
  • латентность, добавленная к цепочке отбрасывания: 4c (Skylake: 5c)
  • пропускная способность: 1 / c (насыщенный p1 и p5). Skylake: 3 / 2c: (3 вектора uops / cycle) / (uop_p01 + uop_p015).
  • «latency», если xmm0 был готов, когда эта последовательность была выпущена (без цикла): тот же

Это должно сработать, но IDK - то, что происходит с NaN. Хорошее наблюдение, что ANDPS является более низкой задержкой и не требует добавления порта FPU.

Это самый маленький размер с не-AVX.


4: сдвиг влево / вправо:

 PSLLD xmm0, 1 PSRLD xmm0, 1 
  • байты: 10 (AVX: 10)
  • fused-domain uops: 2 * n
  • латентность, добавленная к цепочке депо: 4c (2c + байпасные задержки)
  • пропускная способность: 1 / 2c (насыщенный p0, также используемый FP mul). (Skylake 1 / c: удвоенная пропускная способность вектора)
  • «latency», если xmm0 был готов, когда эта последовательность была выпущена (без цикла): тот же

    Это самый маленький (в байтах) с AVX.

    У этого есть возможности, когда вы не можете сэкономить регистр, и он не используется в цикле. (В петле без каких-либо запасных andps xmm0, [mask] использовать andps xmm0, [mask] ).

Я предполагаю, что есть отключение 1 байта от FP до целочисленного сдвига, а затем еще 1c на обратном пути, поэтому это происходит так же медленно, как SUBPS / ANDPS. Он сохраняет uop-порт без выполнения, поэтому он имеет преимущества, если проблема с пропускной способностью промежуточного домена является проблемой, и вы не можете вытащить генерации маски из цикла. (например, потому что это функция, которая вызывается в цикле, а не внутри).


Когда использовать то, что: Загрузка маски из памяти делает код простым, но имеет риск промаха в кеше. И берет 16B данных ro вместо 9 команд байтов.

  • Нужно в цикле: 1c : создать маску вне цикла (с pcmp / shift); используйте один andps внутри. Если вы не можете сэкономить реестр, перелейте его в стек и 1a : andps xmm0, [rsp + mask_local] . (Генерация и хранение с меньшей вероятностью приведет к пропуску кеша, чем константа). Только добавляет 1 цикл к критическому пути в любом случае, с одной командой с одним-хупом внутри цикла. Это port5 uop, поэтому, если ваш цикл насыщает порт перемешивания и не привязан к задержке, PAND может быть лучше. (SnB / IvB перемещает единицы на p1 / p5, но Хасуэлл / Бродвелл / Skylake может только перетасовать на p5. Skylake действительно увеличил пропускную способность для (V)(P)BLENDV , но не для других (V)(P)BLENDV случайном порядке. Если числа AIDA правы, не-AVX BLENDV - 1c lat ~ 3 / c tput, но AVX BLENDV - 2c lat, 1 / c tput (все же улучшение tput по Haswell))

  • Требуется один раз в часто называемой функции без петлирования (поэтому вы не можете амортизировать создание маски в нескольких целях):

    1. Если пропускная способность andps xmm0, [mask] является проблемой: 1a : andps xmm0, [mask] . Случайная прошивка кеша должна быть амортизирована по сбережениям в uops, если это действительно было узким местом.
    2. Если латентность не является проблемой (функция используется только как часть коротких цепочек отрезков, не связанных циклом, например arr[i] = abs(2.0 + arr[i]); ), и вы хотите избежать постоянной в памяти: 4 , потому что это всего 2 раза. Если abs входит в начало или конец цепочки отрезка, от нагрузки или хранилища не будет задержки байпаса.
    3. Если пропускная способность uop не является проблемой: 1c : генерировать «на лету» с целым числом pcmpeq / shift . Отсутствие пропусков в кеше и добавление 1c к критическому пути.
  • Нужно (вне любых циклов) в нерегулярно называемой функции: просто оптимизируйте размер (ни одна маленькая версия не использует константу из памяти). не-AVX: 3 . AVX: 4 . Они неплохие и не могут пропустить кеш-промах. Задержка в 4 циклах хуже для критического пути, чем вы получите с версией 1c, поэтому, если вы не думаете, что 3 байта команд - большое дело, выберите 1c . Версия 4 интересна для ситуаций с давлением в регистре, когда производительность не важна, и вы бы хотели не проливать ничего.


  • Процессоры AMD: есть байпасная задержка в / из ANDPS (которая сама по себе имеет задержку 2 с), но я думаю, что это по-прежнему лучший выбор. Он по-прежнему превосходит задержку 5-6 циклов SUBPS . MAXPS - 2 с. При высоких задержках FP ops на процессорах семейства Bulldozer вы даже более вероятно, что выполнение вне ANDPS может генерировать вашу маску на лету, чтобы она была готова, когда другой операнд ANDPS , Я предполагаю, что Bulldozer через Steamroller не имеет отдельного блока добавления FP, и вместо этого вектор добавляет и умножает в блоке FMA. 3 всегда будет плохой выбор на процессорах AMD Bulldozer. 2 выглядит лучше в этом случае из-за более короткой задержки байпаса от домена fma до домена fp и обратно. См. Руководство по микроархиву Agner Fog, стр. 182 ( 15.11 Задержка данных между различными доменами выполнения ).

  • Silvermont: аналогичные задержки для SnB. По-прежнему идут с 1c для петель и проб. также для одноразового использования. Silvermont вышел из строя, поэтому он может заранее подготовить маску, чтобы добавить только один цикл к критическому пути.

Давайте будем гением компьютера.