В чем разница между _mm_movehdup_ps и _mm_shuffle_ps в этом случае? - PullRequest
3 голосов
/ 21 мая 2019

Если мое понимание верно,

_mm_movehdup_ps(a)

дает тот же результат, что и

_mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 3, 3))?

Есть ли разница в производительностидва?

1 Ответ

4 голосов
/ 21 мая 2019

_MM_SHUFFLE сначала берет старший элемент, поэтому _MM_SHUFFLE(3,3, 1,1) будет делать movshdup shuffle.

Основное отличие - на уровне сборки; movshdup - это функция копирования-и-перемешивания, избегающая movaps для копирования ввода, если ввод a все еще требуется позже (например, как часть горизонтальной суммы: см. Самый быстрый способ сделать горизонтальный вектор с плавающей точкой) сумма на x86 для примера того, как он компилируется без movaps против версии SSE1, которая использует shufps.

movshdup / movsldup также может быть загрузкой + перемешиванием с операндом источника памяти. (shufps, очевидно, не может, потому что ему требуется один и тот же вход дважды.) На современных процессорах Intel (семейство Sandybridge) movshdup xmm0, [rdi] декодирует до чистой нагрузки UOP, а не с микросинхронизацией с ALU UOP . Таким образом, он не конкурирует за пропускную способность ALU (порт 5) с другими шаффлами. Порты загрузки содержат логику для широковещательной загрузки (включая movddup 64-битную широковещательную передачу) и movs[lh]dup дублирование пар элементов. Более сложные load + shuffle, такие как vpermilps xmm0, [rdi], 0x12 или pshufd xmm, [rdi], 0x12, по-прежнему декодируют на несколько мопов, возможно, микросжатые в load + ALU в зависимости от uarch.


Обе инструкции имеют одинаковую длину: movshdup избегает непосредственного байта, но shufps является инструкцией SSE1, поэтому она имеет только 2-байтовый код операции, на 1 байт короче, чем инструкции SSE2 и SSE3. Но с включенным AVX vmovshdup действительно сохраняет байт , потому что преимущество размера кода исчезает.


В более старых процессорах с только 64-разрядными модулями случайного воспроизведения (такими как Pentium-M и Core 2 первого поколения (Merom)) было более значительное преимущество в производительности . movshdup только тасует в пределах 64-битных половинок вектора. В Core 2 Merom movshdup xmm, xmm декодирует до 1 моп, а shufps xmm, xmm, i декодирует до 3 моп. (См. https://agner.org/optimize/ для таблиц инструкций и руководства микроархива). См. Также мой ответ по горизонтальной сумме (связанный ранее) для получения дополнительной информации о процессорах SlowShuffle, таких как Merom и K8.


В C ++ со встроенными функциями

Если SSE3 включен, оптимизация будет пропущена, если ваш компилятор не оптимизирует _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 1, 1)) в ту же сборку, которую он сделал бы для _mm_movehdup_ps(a).

Некоторые компиляторы (например, MSVC), как правило, не оптимизируют intriniscs, поэтому программист должен понять, как избежать использования movaps инструкций, используя встроенные функции для команд копирования и перестановки (например, pshufd) и movshdup) вместо тасов, которые обязательно уничтожают регистр назначения (например, shufps и, например, psrldq byte-shift.)

Кроме того, MSVC не позволяет вам использовать SSE3 компилятором, вы получаете инструкции за пределами базового SSE2 (или без SIMD), если вы используете для них встроенные функции. Или, если вы включите AVX, это позволит компилятору использовать SSE4.2 и более ранние версии, но он все же решит не оптимизировать. Итак, еще раз, до человека-программиста, чтобы найти оптимизации. ICC похож. Иногда это может быть хорошо, если вы знаете точно , что вы делаете, и проверяете вывод asm компилятора, потому что иногда оптимизации gcc или clang могут пессимизировать ваш код.

Вероятно, хорошая идея собрать с clang и посмотреть, использует ли он те же инструкции, что и встроенные в ваш источник; на сегодняшний день он обладает лучшим оптимизатором случайного выбора из всех 4 основных компиляторов, которые поддерживают встроенные функции Intel, в основном оптимизируя код встроенных функций так же, как компиляторы обычно оптимизируют чистый C, то есть просто следуя правилу «как если» для получения того же результата.

Самый тривиальный пример:

#include <immintrin.h>

__m128 shuf1(__m128 a) {
    return _mm_shuffle_ps(a,a, _MM_SHUFFLE(3,3, 1,1));
}

скомпилировано с gcc / clang / MSVC / ICC на Godbolt

GCC и clang с -O3 -march=core2 оба замечают оптимизацию:

shuf1:
        movshdup        xmm0, xmm0
        ret

ICC -O3 -march=haswell и MSVC -O2 -arch:AVX -Gv (для включения соглашения о вызовах векторного вызова вместо передачи векторов SIMD по ссылке.)

shuf1:
        vshufps   xmm0, xmm0, xmm0, 245                         #4.12
        ret                                                     #4.12
...