Каковы наилучшие последовательности команд для генерации векторных констант "на лету"?

"Наилучший" означает наименьшее количество инструкций (или наименьшее количество мопов, если какие-либо инструкции декодируют более чем один моп). Размер машинного кода в байтах - это тай-брейк для равного количества 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, на один байт инструкции короче.) На современных ЦП нет большой разницы, но на Nehalem (до устранения xor-zero), xopps uop мог работать только на порту 5 Я думаю, почему компиляторы предпочитают pxor -zeroing даже для регистров, которые будут использоваться с инструкциями FP.

Все: pcmpeqw xmm0,xmm0. Это обычная отправная точка для генерации других констант, потому что (например, pxor) она нарушает зависимость от предыдущего значения регистра (за исключением старых процессоров, таких как K10 и pre-Core2 P6).

Версия W не имеет преимущества перед версиями pcmpeq размера элемента в байтах или двойном слове на любом процессоре в таблицах команд Agner Fog, но pcmpeqQ занимает дополнительный байт, медленнее в Silvermont и требует SSE4.1.

SO на самом деле не имеет форматирования таблиц, поэтому я просто перечислю дополнения к таблице Agner Fog 13.10, а не улучшенную версию. Сожалею. Возможно, если этот ответ станет популярным, я буду использовать генератор таблиц ascii-art, но, надеюсь, улучшения будут добавлены в будущие версии руководства.


Основная сложность заключается в 8-битных векторах, потому что там нет PSLLB

Таблица Agner Fog генерирует векторы из 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 и является неразрушающим, поэтому у вас все еще есть вектор set1(-1).

(Иногда вам не нужен set1(1). Вы можете добавить 1 к каждому элементу, вычитая -1 вместо psubb.)

Мы можем генерировать 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, чтобы переключиться на отображение векторных регистров, но это почти бесполезно, потому что вы не можете выбрать, какую интерпретацию отображать (вы всегда получаете их все, и не можете прокрутить, и столбцы не выстраиваются между регистрами). Это отлично подходит для целочисленных regs/flags, хотя.


Обратите внимание, что использовать их со встроенными функциями сложно. Компиляторы не любят работать с неинициализированными переменными, поэтому вы должны использовать _mm_undefined_si128(), чтобы сообщить компилятору, что вы имели в виду. Или, возможно, использование _mm_set1_epi32(-1) заставит ваш компилятор испустить pcmpeqd same,same. Без этого некоторые компиляторы будут xor-zero неинициализированные векторные переменные перед использованием или даже (MSVC) загружать неинициализированную память из стека.


Многие константы могут быть более компактно сохранены в памяти, используя преимущества SSE4.1 pmovzx или pmovsx для нуля или расширения знака на лету. Например, 128-битный вектор {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 отлично подходят для сохранения размера исполняемого файла, когда вы собираетесь загружать маску в регистр для повторного использования в цикле. Создание нескольких похожих масок из одной начальной точки также может сэкономить место, если для этого требуется всего одна инструкция.

См. также Что касается вектора SSE, который имеет все те же компоненты, генерирует на лету или выполняет предварительные вычисления?, который задает дополнительные вопросы об использовании встроенной функции set1, и неясно, запрашивает ли она константы или трансляции переменных.

Я также экспериментировал с некоторыми выходами компилятора для трансляций.


Если ошибки в кеше являются проблемой, взгляните на свой код и посмотрите, не дублировал ли компилятор константы _mm_set, когда одна и та же функция встроена в разных вызывающих. Также обратите внимание на то, что константы, которые используются вместе (например, в функциях, вызываемых одна за другой), разбросаны по разным строкам кэша. Многие разбросанные нагрузки для констант намного хуже, чем загрузка множества констант, расположенных рядом друг с другом.

pmovzx и/или широковещательная загрузка позволяют вам упаковать больше констант в строку кэша с очень низкими издержками на их загрузку в регистр. Нагрузка не будет на критическом пути, поэтому, даже если она потребует дополнительного мопа, она может занять свободный исполняющий модуль в любом цикле длинного окна.

clang действительно хорошо справляется с этой задачей: отдельные константы set1 в разных функциях распознаются как идентичные, способ объединения одинаковых строковых литералов. Обратите внимание, что вывод clang asm-источника показывает, что каждая функция имеет свою собственную копию константы, но двоичная разборка показывает, что все эти эффективные RIP-адреса ссылаются на одно и то же местоположение. Для 256-битных версий повторяющихся функций clang также использует vbroadcastsd, чтобы требовать загрузки только 8 Б, за счет дополнительной инструкции в каждой функции. (Это в -O3, поэтому разработчики лягушек поняли, что размер важен для производительности, а не только для -Os). IDK, почему он не сводится к константе 4B с vbroadcastss, потому что это должно быть так же быстро. К сожалению, vbroadcast не просто приходит из части константы 16B, используемой другими функциями. Возможно, это имеет смысл: AVX-версия чего-то может объединить только некоторые его константы с версией SSE. Лучше оставить страницы памяти с SSE-константами полностью холодными, а версия AVX сохранит все свои константы вместе. Кроме того, это более сложная проблема сопоставления с образцом, которую нужно обрабатывать во время сборки или компоновки (однако это было сделано. Я не прочитал каждую директиву, чтобы выяснить, какая из них включает слияние.)

gcc 5.3 также объединяет константы, но не использует широковещательную нагрузку для сжатия констант 32B. Опять же, константа 16B не перекрывается с константой 32B.