Понимание порядка оптимизации для vpermd (или vpermps)

#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-битные векторы), если это всего на несколько% медленнее за крошечную долю времени, которое он тратит на выполнение, если это уменьшает размер двоичного файла.