Каковы лучшие последовательности команд для генерации векторных констант на лету?
"Наилучший" означает наименьшее количество инструкций (или наименьшее количество мопов, если какие-либо инструкции декодируют более чем один моп). Размер машинного кода в байтах является прерывателем для равного количества insn.
Генерация констант по самой своей природе является началом новой цепочки зависимостей, поэтому латентность имеет значение. Также необычно генерировать константы внутри цикла, поэтому требования к пропускной способности и порту выполнения также в основном не имеют значения.
Генерация констант вместо их загрузки требует больше инструкций (за исключением всех нулей или всех единиц), поэтому она потребляет драгоценное пространство uop-cache. Это может быть даже более ограниченный ресурс, чем кэш данных.
Превосходное руководство Agner Fog по оптимизации сборки охватывает это в Section 13.4
, Таблица 13.10 содержит последовательности для генерации векторов, где каждый элемент 0
, 1
, 2
, 3
, 4
, -1
, или же -2
с размерами элементов от 8 до 64 бит. Таблица 13.11 содержит последовательности для генерации некоторых значений с плавающей запятой (0.0
, 0.5
, 1.0
, 1.5
, 2.0
, -2.0
и битовые маски для знака бит.)
Последовательности Agner Fog используют только SSE2, либо по замыслу, либо потому, что он некоторое время не обновлялся.
Какие еще константы можно сгенерировать с помощью коротких неочевидных последовательностей инструкций? (Дальнейшие расширения с различными значениями сдвига очевидны и не "интересны".) Есть ли лучшие последовательности для генерации констант, которые перечисляет Agner Fog?
Как переместить 128-битные немедленные в регистры XMM иллюстрирует несколько способов поместить произвольную 128b-константу в поток команд, но это обычно не имеет смысла (это не экономит место и занимает много места в uop-кэше).
1 ответ
Все равно нулю: pxor xmm0,xmm0
(или же xorps xmm0,xmm0
на одну байт инструкции короче.)
All-один: pcmpeqw xmm0,xmm0
, Это обычная отправная точка для генерации других констант, потому что (как pxor
) он нарушает зависимость от предыдущего значения регистра (за исключением старых процессоров, таких как K10 и pre-Core2 P6). Там нет никакого преимущества для W
версия поверх размера элемента байта или элемента двойного слова pcmpeq
на любом процессоре в таблицах команд Агнер Фог, но pcmpeqQ
занимает дополнительный байт, медленнее в Silvermont и требует SSE4.1.
У SO на самом деле нет форматирования таблиц, поэтому я просто перечислю дополнения к таблице Агнера Фога 13.10, а не улучшенную версию. Сожалею. Возможно, если этот ответ станет популярным, я буду использовать генератор таблиц ascii-art, но, надеюсь, улучшения будут добавлены в будущие версии руководства.
Основная сложность - 8-битные векторы, потому что нет PSLLB
Таблица Агнера Фога генерирует векторы 16-битных элементов и использует packuswb
обойти это. Например, pcmpeqw xmm0,xmm0
/ psrlw xmm0,15
/ psllw xmm0,1
/ packuswb xmm0,xmm0
генерирует вектор, где каждый байт 2
, (Эта схема сдвигов с различными значениями является основным способом получения большинства констант для более широких векторов). Существует лучший способ:
paddb xmm0,xmm0
(SSE2) работает как сдвиг влево на единицу с байтовой гранулярностью, поэтому вектор -2
байты могут быть сгенерированы только с двумя инструкциями (pcmpeqw
/ paddb
). paddw/d/q
так как сдвиг влево на единицу для других размеров элементов экономит один байт машинного кода по сравнению со сдвигами и обычно может работать на большем количестве портов, чем shift-imm.
pabsb xmm0,xmm0
(SSSE3) превращает вектор всех единиц (-1
) в вектор 1
байтов, так что занимает всего две инструкции. Мы можем генерировать 2
байтов с pcmpeqw
/ paddb
/ pabsb
, (Порядок добавления против абс не имеет значения). pabs
не требует imm8, но сохраняет только байты кода для других значений ширины элемента по сравнению со смещением вправо, когда оба требуют 3-байтовый префикс VEX. Это происходит только тогда, когда исходный регистр xmm8-15. (vpabsb/w/d
всегда требуется 3-байтовый префикс VEX для VEX.128.66.0F38.WIG
, но vpsrlw dest,src,imm
в противном случае можно использовать 2-байтовый префикс VEX для его VEX.NDD.128.66.0F.WIG
).
Мы действительно можем сохранить инструкции в генерации 4
байтов тоже: pcmpeqw
/ pabsb
/ psllw xmm0, 2
, Все биты, которые сдвинуты через границы байтов при сдвиге слов, равны нулю, благодаря pabsb
, Очевидно, что другие счетчики сдвигов могут поместить один установленный бит в другие местоположения, включая знаковый бит, чтобы сгенерировать вектор -128 (0x80) байтов. Обратите внимание, что pabsb
является неразрушающим (целевой операнд доступен только для записи и не должен совпадать с исходным для получения желаемого поведения). Вы можете хранить все единицы как константу, или как начало генерации другой константы, или как исходный операнд для psubb
(увеличить на единицу).
Вектор 0x80
байты также можно (см. предыдущий абзац) генерировать из всего, что насыщается до -128, используя packsswb
, например, если у вас уже есть вектор 0xFF00
для чего-то другого, просто скопируйте его и используйте packsswb
, Константы, загруженные из памяти и правильно насыщенные, являются потенциальными целями для этого.
Вектор 0x7f
байты могут быть сгенерированы с pcmpeqw
/ psrlw xmm0, 9
/ packuswb xmm0,xmm0
, Я считаю это "неочевидным", потому что в основном установленный характер не заставил меня думать о том, чтобы просто генерировать это как значение в каждом слове и делать обычное packuswb
,
pavgb
(SSE2) против обнуленного регистра может сдвиг вправо на единицу, но только если значение четное. (Это без знака dst = (dst+src+1)>>1
для округления, с 9-битной внутренней точностью для временного.) Это не кажется полезным для генерации констант, хотя, потому что 0xff является нечетным: pxor xmm1,xmm1
/ pcmpeqw xmm0,xmm0
/ paddb xmm0,xmm0
/ pavgb xmm0, xmm1
производит 0x7f
байтов с одним insn больше чем shift/pack. Если нулевой регистр уже нужен для чего-то еще, paddb
/ pavgb
сохраняет один байт инструкции.
Я проверил эти последовательности. Самый простой способ - бросить их в .asm
, собрать / связать, и запустить GDB на нем. layout asm
, display /x $xmm0.v16_int8
сбросить это после каждого одношагового и одношагового инструкций (ni
или же si
). В layout reg
режим, вы можете сделать tui reg vec
переключиться на отображение векторных регистров, но это почти бесполезно, потому что вы не можете выбрать, какую интерпретацию отображать (вы всегда получаете их все и не можете прокрутить, а столбцы не выстраиваются между регистрами). Это отлично подходит для целочисленных регистров / флагов.
Обратите внимание, что использовать их со встроенными функциями может быть сложно. Компиляторы не любят работать с неинициализированными переменными, поэтому вы должны использовать _mm_undefined_si128()
сказать компилятору, что вы имели в виду. Или, возможно, используя _mm_set1_epi32(-1)
заставит ваш компилятор выдавать pcmpeqd same,same
, Без этого некоторые компиляторы будут xor-zero неинициализированные векторные переменные перед использованием или даже (MSVC) загружать неинициализированную память из стека.
Многие константы могут быть более компактно сохранены в памяти, используя преимущества SSE4.1 pmovzx
или же pmovsx
для нуля или расширения знака на лету. Например, вектор 128b {1, 2, 3, 4}
так как 32-битные элементы могут быть созданы с pmovzx
загрузить из 32-битной ячейки памяти. Операнды памяти могут слиться с pmovzx
, так что это не займет никаких дополнительных мопов слитых доменов. Однако он не позволяет использовать константу непосредственно как операнд памяти.
Встроенная поддержка C/C++ для использования pmovz/sx
как груз ужасен: есть _mm_cvtepu8_epi32 (__m128i a)
, но нет версии, которая принимает uint32_t *
операнд указателя. Вы можете взломать его, но это уродливо, и сбой оптимизации компилятора является проблемой. См. Связанный вопрос для деталей и ссылок на отчеты об ошибках gcc.
С 256b и (не так) скоро 512b константами, экономия памяти больше. Это очень важно, только если несколько полезных констант могут совместно использовать строку кэша.
FP эквивалент этого VCVTPH2PS xmm1, xmm2/m64
, требующий флаг функции F16C (половина точности). (Есть также инструкция хранения, которая упаковывает один к половине, но нет вычислений с половинной точностью. Это только оптимизация пропускной способности памяти / кэша.)
Очевидно, что когда все элементы одинаковы (но не подходят для генерации на лету), pshufd
или AVX vbroadcastps
/ AVX2 vpbroadcastb/w/d/q/i128
полезны pshufd
может принимать операнд источника памяти, но он должен быть 128b. movddup
(SSE3) выполняет 64-битную загрузку, транслируя для заполнения 128-битного регистра. На Intel не требуется исполнительный блок ALU, только порт загрузки. (Аналогично, AVX v[p]broadcast
грузы размера меча и больше обрабатываются в единице нагрузки, без ALU).
Трансляции или pmovz/sx
отлично подходят для сохранения размера исполняемого файла, когда вы собираетесь загрузить маску в регистр для повторного использования в цикле. Создание нескольких похожих масок из одной начальной точки также может сэкономить место, если для этого требуется только одна инструкция.
Смотрите также For для SSE-вектора, который имеет все те же компоненты, генерирует на лету или предварительно вычисляет? который просит больше об использовании set1
внутренне, и не ясно, спрашивает ли он о константах или трансляциях переменных.
Я также экспериментировал с выводом компилятора для трансляций.
Если ошибки в кеше являются проблемой, посмотрите на ваш код и посмотрите, не дублируется ли компилятор _mm_set
константы, когда одна и та же функция встроена в разных вызывающих. Также обратите внимание на то, что константы, которые используются вместе (например, в функциях, вызываемых одна за другой), разбросаны по разным строкам кэша. Многие разбросанные нагрузки для констант намного хуже, чем загрузка множества констант, расположенных рядом.
pmovzx
и / или широковещательная загрузка позволяет вам упаковать больше констант в строку кэша с очень низкими издержками на их загрузку в регистр. Нагрузка не будет находиться на критическом пути, поэтому, даже если она потребует дополнительного мопа, она может занять свободный исполняющий модуль в любом цикле длинного окна.
Clang на самом деле делает хорошую работу по этому: отделить set1
константы в разных функциях распознаются как идентичные, способ объединения одинаковых строковых литералов. Обратите внимание, что выходные данные asm-источника clang показывают, что каждая функция имеет свою собственную копию константы, но двоичная разборка показывает, что все эти эффективные RIP-адреса ссылаются на одно и то же местоположение. Для 256-битных версий повторяющихся функций clang также использует vbroadcastsd
требовать только 8B нагрузки за счет дополнительной инструкции в каждой функции. (Это в -O3
Очевидно, что разработчики Clang поняли, что размер имеет значение для производительности, а не только для -Os
). ИДК, почему он не падает до константы 4B с vbroadcastss
потому что это должно быть так же быстро. К сожалению, vbroadcast не просто приходит из части константы 16B, используемой другими функциями. Возможно, это имеет смысл: AVX-версия чего-то могла бы объединить только некоторые его константы с версией SSE. Лучше оставить страницы памяти с константами SSE полностью холодными, а версия AVX сохранит все свои константы вместе. Кроме того, это более сложная проблема сопоставления с образцом, которая должна быть решена во время сборки или соединения (однако это сделано. Я не прочитал каждую директиву, чтобы выяснить, какая из них включает слияние).
GCC 5.3 также объединяет константы, но не использует широковещательную нагрузку для сжатия 32B констант. Снова константа 16B не перекрывается с константой 32B.