#assembly #x86-64 #avx #micro-optimization
#сборка #x86-64 #avx #микрооптимизация
Вопрос:
В принципе, предполагая, что у вас есть список индексов перестановок во время компиляции, я пытаюсь понять наилучший порядок выбора команд для x86_64.
Я понимаю большинство вариантов оптимизации Agner Fog, но есть один случай, который мне трудно понять.
Задан порядок перестановок, который может быть реализован как;
_mm256_permutevar8x32_epi32(r, _mm256_set_epi32(/* indicies */));
или
__m256i tmp = _mm256_permute4x64_epi64(r, /* some mask */);
return _mm256_shuffle_epi32(tmp, /* another mask */);
Я не понимаю, почему первый вариант когда-либо будет лучше.
Возьмем пример списка перестановок 7, 6, 5, 4, 3, 2, 1, 0
(обратный epi32)
__m256i
load_perm(__m256i r) {
// clang
// 1 uop vmovaps (y, m) p23
// 1 uop vpermps (y, y, y) p5
// gcc
// 1 uop vmovdqa (y, m) p23
// 1 uop vpermd (y, y, y) p5
return _mm256_permutevar8x32_epi32(r, _mm256_set_epi32(0, 1, 2, 3, 4, 5, 6, 7));
}
__m256i
perm_shuf(__m256i r) {
// clang
// 1 uop vmovaps (y, m) p23
// 1 uop vpermps (y, y, y) p5
// gcc
// 1 uop vpermq (y, y, i) p5
// 1 uop vpshufd (y, y, i) p5
__m256i tmp = _mm256_permute4x64_epi64(r, 0x4e);
return _mm256_shuffle_epi32(tmp, 0x1b);
}
Для обоих вариантов требуется 2 uop, и, учитывая, что между двумя инструкциями существует зависимость, я не думаю, что порты действительно имеют значение. Единственное различие, которое я вижу, заключается в том, что первый вариант добавляет дополнительные 32 байта .rodata .
Кто-нибудь может помочь мне понять, почему Clang (и, я думаю, Agner Fog) предпочитают первый вариант второму?
вот ссылка на godbolt с результатами компиляции для skylake
Ответ №1:
Для load_perm
clang, похоже, нравится превращать вещи в ps
форму. Это экономит размер кода для устаревшей кодировки SSE (где инструкции SSE1 имеют меньше префиксов). Но не с кодировками VEX, поэтому нет никаких преимуществ. Просто оптимизатор clang shuffle, по-видимому, не знает или не заботится о сохранении целого числа против Различие домена FP. Что, я думаю, подходит для перетасовки на текущих процессорах.
Для perm_shuf
этого, безусловно, оптимизатор clang shuffle выполняет свою работу. Другие компиляторы менее хороши в обработке встроенных функций shuffle так же, как они обрабатывают
*
операторы and: как способы указания желаемого результата без обязательного указания способа его получения. например x * y
, не нужно компилировать imul
для x86, и выбор может зависеть от окружающего кода.
Большая часть кода SIMD выполняется в циклах, поэтому неплохо предположить, что константа shuffle будет оставаться горячей в кэше и использоваться несколько раз. Особенно, если эти строки и вектор перетасовки могут быть подняты. Но даже если нет, может стоить загрузить константу. Одна перетасовка лучше, чем 2, для задержки критического пути от m
ввода до return
значения, а также для операций ввода-вывода на 5 портов на процессорах Intel (обычно ограничивается 1 перетасовкой за такт начиная с Haswell и далее, до Ice Lake.)
Кстати, m
это действительно плохой выбор имени переменной: оно поступает в регистр, и вы используете m
в своих комментариях, чтобы говорить о константах памяти.
Комментарии:
1. Хороший вызов, изменен
m
наr
. Но разве нагрузка также не находится на критическом пути? Согласно таблице инструкций Агнера Фогаvmovaps
, она имеет ту же задержку,vpshufd
что и, иvpermq
поэтому не понимаю, как компенсируется 32 байта .rodata (и раздувание исполняемого файла). Также что вы подразумеваете под «перетасовкой вектора может быть поднят»? Вы имеете в виду повторное использование одного и того же регистра, в который он был загружен несколько раз?2. @Noah: Нет, адрес загрузки фактически является постоянной константой, доступной как часть декодирования инструкции. (RIP-относительный или 32-разрядный абсолютный в зависимости от режима.) Uop загрузки может быть уже выполнен до
v
(данные перетасовываются) готов, поэтому толькоvpermd
задержка является частью критического пути отv
результата . Конечно, если при вызове этой функции произошла ошибка I-cache или что-то еще, загрузка не могла начаться раньше времени, и / или загрузка векторных данных могла отсутствовать в кэше.3. @Noah: Очевидно, что намного лучше, если мы говорим о цикле, в котором может быть поднята постоянная нагрузка.
vmovaps
.rodata
извне цикла,vpermps
внутри цикла. Тогда у вас есть только 1 общий uop для перетасовки, и любой риск промаха кэша амортизируется по количеству итераций цикла. en.wikipedia.org/wiki/Loop-invariant_code_motion#Benefits И, кстати, если вы загружаете только один раз, вы можете сжать константу перемешивания до 8 байт, загрузив ее с помощьюvpmovzxbd
4. Ах, я понимаю. В качестве примечания мне интересно, почему Агнер Фог решил сделать
_mm256_shufflelo_epi16
и_mm256_shufflehi_epi16
раньше_mm256_shuffle_epi8
в своей векторной библиотеке здесь . Есть идеи? например, он отдает приоритет случаю, когда вы нажимаете оба выше одного случайного epi8.5. @Noah: Да, это довольно разумно. Хотя вы можете считать цикл «горячим», даже если он на самом деле выполняется не часто. Особенно, если количество итераций — это те несколько раз, когда оно вводится, поэтому вы амортизируете нагрузку при большом количестве применений. Если код действительно холодный, вам часто не следует его векторизировать в первую очередь или делать это более компактным способом (например, 128-битные векторы), если это всего на несколько% медленнее за крошечную долю времени, которое он тратит на выполнение, если это уменьшает размер двоичного файла.