Как очистить верхние 128 бит значения __m256?

Как очистить верхние 128 бит m2:

__m256i    m2 = _mm256_set1_epi32(2);
__m128i    m1 = _mm_set1_epi32(1);

m2 = _mm256_castsi128_si256(_mm256_castsi256_si128(m2));
m2 = _mm256_castsi128_si256(m1);

не работает. Документация Intels для _mm256_castsi128_si256 intrinsic говорит, что "верхние биты результирующего вектора undefined". В то же время я могу легко сделать это в сборке:

VMOVDQA xmm2, xmm2  //zeros upper ymm2
VMOVDQA xmm2, xmm1

Конечно, я бы не хотел использовать "и" или _mm256_insertf128_si256() и т.д.

Ответ 1

Обновление: теперь есть __m128i _mm256_zextsi128_si256(__m128i) ; см. ответ Агнера Туманного. Оставшаяся часть ответа, приведенного ниже, относится только к старым компиляторам, которые не поддерживают эту встроенную функцию и где нет эффективного, переносимого решения.


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

Есть несколько основных способов, которыми мы могли бы написать это:

Версия А:

ymm = _mm256_set_m128i(_mm_setzero_si128(), _mm256_castsi256_si128(ymm));

Версия Б:

ymm = _mm256_blend_epi32(_mm256_setzero_si256(),
                         ymm,
                         _MM_SHUFFLE(0, 0, 3, 3));

Версия C:

ymm = _mm256_inserti128_si256(_mm256_setzero_si256(),
                              _mm256_castsi256_si128(ymm),
                              0);

Каждый из них делает именно то, что мы хотим, очищая верхние 128 бит 256-битного регистра YMM, чтобы можно было безопасно использовать любой из них. Но какой самый оптимальный? Ну, это зависит от того, какой компилятор вы используете...

GCC:

Версия A: совсем не поддерживается, поскольку в GCC отсутствует встроенная _mm256_set_m128i. (Конечно, можно смоделировать, но это можно сделать, используя одну из форм в "B" или "C".)

Версия B: Скомпилировано с неэффективным кодом. Идиома не распознается, а встроенные символы очень буквально переводятся в инструкции машинного кода. Временный регистр VPXOR обнуляется с использованием VPXOR, а затем он смешивается с входным регистром VPBLENDD с использованием VPBLENDD.

Версия C: Идеально. Хотя код выглядит довольно пугающим и неэффективным, все версии GCC, поддерживающие генерацию кода AVX2, распознают эту идиому. Вы получаете ожидаемый VMOVDQA xmm?, xmm? инструкция, которая неявно очищает верхние биты.

Предпочитаю версию C!

Лязг:

Версия A: Скомпилировано с неэффективным кодом. Временный регистр VPXOR обнуляется с помощью VPXOR, а затем он вставляется во временный регистр VINSERTI128 с помощью VINSERTI128 (или форм с плавающей запятой, в зависимости от версии и параметров).

Версия B & C: также скомпилирована для неэффективного кода. Временный регистр YMM снова обнуляется, но здесь он смешивается с входным регистром VPBLENDD с использованием VPBLENDD.

Ничего идеального!

ICC:

Версия A: Идеально. Производит ожидаемый VMOVDQA xmm?, xmm? инструкция.

Версия B: Скомпилировано с неэффективным кодом. Обнуляет временный регистр YMM, а затем смешивает нули с входным регистром VPBLENDD (VPBLENDD).

Версия C: также скомпилирована с неэффективным кодом. Обнуляет временный регистр VINSERTI128, а затем использует VINSERTI128 для вставки нулей во временный регистр YMM.

Предпочитаю версию A!

MSVC:

Версия A и C: скомпилированы с неэффективным кодом. Обнуляет временный регистр VINSERTI128, а затем использует VINSERTI128 (A) или VINSERTF128 (C) для вставки нулей во временный регистр YMM.

Версия B: также скомпилирована с неэффективным кодом. Обнуляет временный регистр YMM, а затем смешивает его с входным регистром VPBLENDD используя VPBLENDD.

Ничего идеального!


Таким образом, можно использовать GCC и ICC для VMOVDQA идеальной инструкции VMOVDQA, если вы используете правильную кодовую последовательность. Но я не вижу способа заставить Clang или MSVC безопасно VMOVDQA инструкцию VMOVDQA. Этим компиляторам не хватает возможности оптимизации.

Итак, на Clang и MSVC у нас есть выбор между XOR + blend и XOR + insert. Что лучше? Обратимся к таблицам инструкций Agner Fog (также доступна версия электронной таблицы):

На архитектуре AMD Ryzen: (Семейство Bulldozer аналогично для AVX __m256 эквивалентов и для AVX2 на экскаваторе):

  Instruction   | Ops | Latency | Reciprocal Throughput |   Execution Ports
 ---------------|-----|---------|-----------------------|---------------------
   VMOVDQA      |  1  |    0    |          0.25         |   0 (renamed)
   VPBLENDD     |  2  |    1    |          0.67         |   3
   VINSERTI128  |  2  |    1    |          0.67         |   3

Агнер Фог, кажется, пропустил некоторые инструкции AVX2 в разделе Ryzen своих таблиц. Посмотрите этот результат AIDA64 InstLatX64 для подтверждения того, что VPBLENDD ymm выполняет то же самое, что VPBLENDW ymm на Ryzen, а не то же самое, что VBLENDPS ymm (пропускная способность 1c от 2 мопов, которые могут работать на 2 портах).

См. Также Excavator/Carrizo InstLatX64, показывающий, что VPBLENDD и VINSERTI128 имеют одинаковую производительность (задержка 2 цикла, 1 на пропускную способность цикла). То же самое для VBLENDPS/VINSERTF128.

На архитектурах Intel (Haswell, Broadwell и Skylake):

  Instruction   | Ops | Latency | Reciprocal Throughput |   Execution Ports
 ---------------|-----|---------|-----------------------|---------------------
   VMOVDQA      |  1  |   0-1   |          0.33         |   3 (may be renamed)
   VPBLENDD     |  1  |    1    |          0.33         |   3
   VINSERTI128  |  1  |    3    |          1.00         |   1

Очевидно, что VMOVDQA является оптимальным как для AMD, так и для Intel, но мы уже знали об этом, и, по-видимому, это не вариант ни для Clang, ни для MSVC, пока их генераторы кода не будут улучшены для распознавания одного из вышеперечисленных идиом или дополнительного добавлено для этой конкретной цели.

К счастью, VPBLENDD по крайней мере так же хорош, как VINSERTI128 на процессорах AMD и Intel. На процессорах Intel VPBLENDD является значительным улучшением по сравнению с VINSERTI128. (На самом деле, это почти так же хорошо, как VMOVDQA в редком случае, когда последний не может быть переименован, за исключением необходимости использования константы с нулевым вектором.) Предпочитайте последовательность встроенных функций, которая приводит к инструкции VPBLENDD если вы не можете уговорить ваш компилятор для использования VMOVDQA.

Если вам нужна версия с плавающей точкой __m256 или __m256d, выбор будет более сложным. На Ryzen VBLENDPS имеет пропускную способность 1c, а VINSERTF128 - 0.67c. На всех других процессорах (включая семейство AMD Bulldozer) VBLENDPS равен или лучше. Это намного лучше на Intel (так же, как для целых чисел). Если вы оптимизируете специально для AMD, вам может потребоваться провести больше тестов, чтобы увидеть, какой вариант является самым быстрым в вашей конкретной последовательности кода, в противном случае смешайте. Это только немного хуже на Ryzen.

Итак, нацелившись на общий x86 и поддерживая как можно больше различных компиляторов, мы можем сделать следующее:

#if (defined _MSC_VER)

    ymm = _mm256_blend_epi32(_mm256_setzero_si256(),
                             ymm,
                             _MM_SHUFFLE(0, 0, 3, 3));

#elif (defined __INTEL_COMPILER)

    ymm = _mm256_set_m128i(_mm_setzero_si128(), _mm256_castsi256_si128(ymm));

#elif (defined __GNUC__)

    // Intended to cover GCC and Clang.
    ymm = _mm256_inserti128_si256(_mm256_setzero_si256(),
                                  _mm256_castsi256_si128(ymm),
                                  0);

#else
    #error "Unsupported compiler: need to figure out optimal sequence for this compiler."
#endif

Смотрите это и версии A, B и C отдельно в проводнике компилятора Godbolt.

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

Ответ 2

Для решения этой проблемы была добавлена новая встроенная функция:

m2 = _mm256_zextsi128_si256(m1);

https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=_mm256_zextsi128_si256&expand=6177,6177

Эта функция не производит никакого кода, если верхняя половина уже известна как ноль, она просто гарантирует, что верхняя половина не рассматривается как неопределенная.

Ответ 3

Посмотрите, что генерирует ваш компилятор для этого:

__m128i m1 = _mm_set1_epi32(1);
__m256i m2 = _mm256_set_m128i(_mm_setzero_si128(), m1);

или, альтернативно, это:

__m128i m1 = _mm_set1_epi32(1);
__m256i m2 = _mm256_setzero_si256();
m2 = _mm256_inserti128_si256 (m2, m1, 0);

Версия clang, которая у меня есть, похоже, генерирует тот же код для (vxorps + vinsertf128), но YMMV.