Есть ли более эффективный способ транслировать 4 смежных двойных значения в 4 регистра YMM?

В фрагменте кода C++, который делает что-то похожее (но не совсем) на умножение матриц, я загружаю 4 смежных двойных числа в 4 регистра YMM следующим образом:

# a is a 64-byte aligned array of double
__m256d b0 = _mm256_broadcast_sd(&b[4*k+0]);
__m256d b1 = _mm256_broadcast_sd(&b[4*k+1]);
__m256d b2 = _mm256_broadcast_sd(&b[4*k+2]);
__m256d b3 = _mm256_broadcast_sd(&b[4*k+3]);

Я скомпилировал код с помощью gcc-4.8.2 на машине Sandy Bridge. Счетчики аппаратных событий (Intel PMU) предполагают, что ЦП фактически выдает 4 отдельные загрузки из кэша L1. Хотя на данный момент я не ограничен задержкой или пропускной способностью L1, мне очень интересно узнать, есть ли способ загрузить 4 двойника с одной 256-битной загрузкой (или двумя 128-битными загрузками) и перетасовать их в 4 регистра YMM. Я просмотрел Руководство по Intel Intrinsics, но не смог найти способ выполнить перетасовку обязательный. Это возможно?

(Если предположение о том, что ЦП не объединяет 4 последовательные загрузки, на самом деле неверно, сообщите мне об этом.)


person netvope    schedule 13.05.2014    source источник
comment
Вы можете сделать 2 x _mm256_broadcast_pd и 4 x _mm256_shuffle_pd — вы сэкономите две загрузки, но добавите две инструкции. Хотя я сомневаюсь, что это будет иметь большое значение.   -  person Paul R    schedule 13.05.2014
comment
@PaulR, я опубликовал ответ, используя 2x mm256_permute2f128_pd и 4x _mm256_permute_pd. Не знаю, лучше ли это вашего предложения. Но в узких петлях нагрузка может быть убийственной.   -  person Z boson    schedule 13.05.2014
comment
Спасибо вам обоим. В настоящее время это немного замедляет мой код, но уменьшает количество загрузок до 1/4. Этот метод будет неоценим, когда я стеснен нагрузками.   -  person netvope    schedule 13.05.2014
comment
На Haswell и более поздних версиях: широковещательные нагрузки (_mm256_broadcast_sd) имеют еще большее преимущество, см. мой ответ.   -  person Peter Cordes    schedule 23.05.2016


Ответы (3)


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

#include <stdio.h>
#include <immintrin.h>

int main() {
    double in[] = {1,2,3,4};
    double out[4];
    __m256d x4 = _mm256_loadu_pd(in);
    __m256d t1 = _mm256_permute2f128_pd(x4, x4, 0x0);
    __m256d t2 = _mm256_permute2f128_pd(x4, x4, 0x11);
    __m256d broad1 = _mm256_permute_pd(t1,0);
    __m256d broad2 = _mm256_permute_pd(t1,0xf);
    __m256d broad3 = _mm256_permute_pd(t2,0);
    __m256d broad4 = _mm256_permute_pd(t2,0xf);

    _mm256_storeu_pd(out,broad1);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad2);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad3);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad4);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
}

Изменить. Вот еще одно решение, основанное на предложении Пола Р.

__m256 t1 = _mm256_broadcast_pd((__m128d*)&b[4*k+0]);
__m256 t2 = _mm256_broadcast_pd((__m128d*)&b[4*k+2]);
__m256d broad1 = _mm256_permute_pd(t1,0);
__m256d broad2 = _mm256_permute_pd(t1,0xf);
__m256d broad3 = _mm256_permute_pd(t2,0);
__m256d broad4 = _mm256_permute_pd(t2,0xf);
person Z boson    schedule 13.05.2014
comment
Спасибо. Это отлично работает на gcc-4.8.2, но gcc-4.4.7 жалуется, что в _mm256_permute_pd() последний аргумент должен быть 4-битным немедленным. Изменение 0xff на 0xf исправляет это. - person netvope; 13.05.2014
comment
Что касается вашего редактирования: требуется приведение типа (по крайней мере, на gcc-4.8.2). то есть это работает: __m256d b0101 = _mm256_broadcast_pd((__m128d*)&b[4*k]); - person netvope; 14.05.2014
comment
@netvope, хорошо, я исправил и протестировал. Оно работает! - person Z boson; 14.05.2014

TL;DR: почти всегда лучше просто выполнить четыре широковещательных загрузки, используя _mm256_set1_pd(). Это очень хорошо для Haswell и более поздних версий, где vbroadcastsd ymm,[mem] не требует операции тасования ALU, и обычно также является лучшим вариантом для Sandybridge/Ivybridge (где это инструкция загрузки из 2 операций + тасование).

Это также означает, что вам вообще не нужно заботиться о выравнивании, кроме естественного выравнивания для double.

Первый вектор будет готов раньше, чем если бы вы выполнили двухэтапную загрузку + перетасовку, поэтому потенциально неупорядоченное выполнение может начаться с кода, использующего эти векторы, в то время как первый все еще загружается. AVX512 может даже складывать широковещательные нагрузки в операнды памяти для инструкций ALU, поэтому такой способ позволит перекомпилировать небольшое преимущество AVX512 с векторами 256b.


(Обычно лучше использовать set1(x), а не _mm256_broadcast_sd(&x). Если форма регистра-источника vbroadcastsd только для AVX2 недоступна, компилятор может выбрать сохранение -> широковещательная загрузка или два перетасовывания. означает, что ваш код будет работать на входах, которые уже находятся в регистрах.)

Если вы действительно ограничены конфликтами ресурсов порта загрузки или пропускной способности, а не общим количеством мопов или ресурсов ALU/shuffle, это может помочь заменить пару широковещательных передач 64-> 256b на широковещательную нагрузку 16B-> 32B (vbroadcastf128/ _mm256_broadcast_pd) и две перетасовки на дорожке (vpermilpd или vunpckl/hpd (_mm256_shuffle_pd)).

Или с AVX2: загрузите 32 байта и используйте 4 _mm256_permute4x64_pd перетасовки для трансляции каждого элемента в отдельный вектор.


Источник таблицы insn Agner Fog (и microarch pdf):

Intel Haswell и более поздние версии:

vbroadcastsd ymm,[mem] и другие инструкции широковещательной загрузки - это инструкции 1uop, которые полностью обрабатываются загрузочным портом (трансляция происходит "бесплатно").

Общая стоимость выполнения четырех широковещательных нагрузок таким образом составляет 4 инструкции. слитный домен: 4 мкп. unfused-domain: 4 мопса для p2/p3. Производительность: два вектора за цикл.

Haswell имеет только одно устройство тасования на порту 5. Выполнение всех ваших широковещательных нагрузок с load+shuffle будет узким местом на p5.

Максимальная пропускная способность вещания, вероятно, достигается при сочетании vbroadcastsd ymm,m64 и перетасовки:

## Haswell maximum broadcast throughput with AVX1
vbroadcastsd    ymm0, [rsi]
vbroadcastsd    ymm1, [rsi+8]
vbroadcastf128  ymm2, [rsi+16]     # p23 only on Haswell, also p5 on SnB/IvB
vunpckhpd       ymm3, ymm2,ymm2
vunpcklpd       ymm2, ymm2,ymm2
vbroadcastsd    ymm4, [rsi+32]     # or vaddpd ymm0, [rdx+something]
#add             rsi, 40

Любой из этих режимов адресации может быть двухрегистровым индексным режимом адресации, потому что не нужно микрофьюзить, чтобы быть единой uop.

AVX1: 5 векторов за 2 цикла, насыщая p2/p3 и p5. (Игнорирование разделения строк кэша при загрузке 16 байт). 6 объединенных доменов, оставляя только 2 мопов на 2 цикла для использования 5 векторов... Реальный код, вероятно, использовал бы часть пропускной способности загрузки для загрузки чего-то еще (например, нешироковещательная загрузка 32B из другого массива, возможно, как операнд памяти в инструкцию ALU), или оставить место для сохранения, чтобы украсть p23 вместо использования p7.

## Haswell maximum broadcast throughput with AVX2
vmovups         ymm3, [rsi]
vbroadcastsd    ymm0, xmm3         # special-case for the low element; compilers should generate this from _mm256_permute4x64_pd(v, 0)
vpermpd         ymm1, ymm3, 0b01_01_01_01   # NASM syntax for 0x99
vpermpd         ymm2, ymm3, 0b10_10_10_10
vpermpd         ymm3, ymm3, 0b11_11_11_11
vbroadcastsd    ymm4, [rsi+32]
vbroadcastsd    ymm5, [rsi+40]
vbroadcastsd    ymm6, [rsi+48]
vbroadcastsd    ymm7, [rsi+56]
vbroadcastsd    ymm8, [rsi+64]
vbroadcastsd    ymm9, [rsi+72]
vbroadcastsd    ymm10,[rsi+80]    # or vaddpd ymm0, [rdx + whatever]
#add             rsi, 88

AVX2: 11 векторов за 4 цикла, насыщение p23 и p5. (Игнорируя разбиение строки кэша для загрузки 32 байт...). Слитный домен: 12 моп, оставляя 2 моп на каждые 4 цикла сверх этого.

Я думаю, что невыровненные загрузки 32B немного более уязвимы с точки зрения производительности, чем невыровненные загрузки 16B, такие как vbroadcastf128.


Интел СНБ/ИвБ:

vbroadcastsd ymm, m64 — это 2 объединенных домена uop: p5 (перетасовка) и p23 (загрузка).

vbroadcastss xmm, m32 и movddup xmm, m64 предназначены только для порта загрузки одиночной операции. Интересно, что vmovddup ymm, m256 также является однократной инструкцией загрузки только для порта, но, как и все загрузки 256b, она занимает порт загрузки на 2 такта. Он все еще может генерировать адрес магазина во 2-м цикле. Однако этот uarch плохо справляется с разделением строк кэша для невыровненных 32-битных загрузок. gcc по умолчанию использует movups / vinsertf128 для невыровненных 32-байтных загрузок с -mtune=sandybridge / -mtune=ivybridge.

4-кратная широковещательная нагрузка: 8 объединенных доменных операций: 4 p5 и 4 p23. Пропускная способность: 4 вектора на 4 цикла, узкое место на порту 5. Множественные загрузки из одной и той же строки кэша в одном и том же цикле не вызывают конфликта между кэшем и банком, так что это и близко не к насыщению портов загрузки (также необходимо для адресов хранения). поколение). Это происходит только в одном и том же банке двух разных строк кэша в одном и том же цикле.

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

У SnB есть 2 блока тасования, но только тот, что на p5, может обрабатывать тасование, имеющее версию 256b в AVX. Использование uop целочисленного тасования p1 для передачи двойного значения обоим элементам регистра xmm ни к чему не приведет, так как vinsertf128 ymm,ymm,xmm,i использует uop p5 тасования.

## Sandybridge maximum broadcast throughput: AVX1
vbroadcastsd    ymm0, [rsi]
add             rsi, 8

один за такт, насыщая p5, но используя только половину емкости p23.

Мы можем сэкономить один uop загрузки за счет еще 2 uop перемешивания, пропускная способность = два результата за 3 такта:

vbroadcastf128  ymm2, [rsi+16]     # 2 uops: p23 + p5 on SnB/IvB
vunpckhpd       ymm3, ymm2,ymm2    # 1 uop: p5
vunpcklpd       ymm2, ymm2,ymm2    # 1 uop: p5

Выполнение загрузки 32B и распаковка с помощью 2x vperm2f128 -> 4x vunpckh/lpd может помочь, если магазины являются частью того, что конкурирует за p23.

person Peter Cordes    schedule 23.05.2016

Вот вариант, основанный на исходном ответе Z Boson (до редактирования), с использованием двух 128-битных загрузок вместо одной 256-битной загрузки.

__m256d b01 = _mm256_castpd128_pd256(_mm_load_pd(&b[4*k+0]));
__m256d b23 = _mm256_castpd128_pd256(_mm_load_pd(&b[4*k+2]));
__m256d b0101 = _mm256_permute2f128_pd(b01, b01, 0);
__m256d b2323 = _mm256_permute2f128_pd(b23, b23, 0);
__m256d b0000 = _mm256_permute_pd(b0101, 0);
__m256d b1111 = _mm256_permute_pd(b0101, 0xf);
__m256d b2222 = _mm256_permute_pd(b2323, 0);
__m256d b3333 = _mm256_permute_pd(b2323, 0xf);

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


Изменить: gcc компилирует две загрузки, а первые 2 переставляются в

vmovapd (%rdi),%xmm8
vmovapd 0x10(%rdi),%xmm4
vperm2f128 $0x0,%ymm8,%ymm8,%ymm1
vperm2f128 $0x0,%ymm4,%ymm4,%ymm2

Предложение Пола Р. об использовании _mm256_broadcast_pd() можно записать так:

__m256d b0101 = _mm256_broadcast_pd((__m128d*)&b[4*k+0]);
__m256d b2323 = _mm256_broadcast_pd((__m128d*)&b[4*k+2]);

который компилируется в

vbroadcastf128 (%rdi),%ymm6
vbroadcastf128 0x10(%rdi),%ymm11

и быстрее, чем два vmovapd+vperm2f128 (проверено).

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

person netvope    schedule 13.05.2014
comment
Вы можете сохранить первые две из этих перестановок, используя 2 x _mm256_broadcast_pd вместо двух загрузок и двух перестановок, что фактически сведет ваше решение к моему первоначальному предложению в комментариях выше. Мне было бы интересно узнать, как это соотносится с альтернативами с точки зрения производительности. - person Paul R; 13.05.2014
comment
@PaulR, теперь я понимаю твою точку зрения. Я обновил свой ответ на основе вашего предложения. Я не могу проверить это прямо сейчас, но буду завтра. - person Z boson; 13.05.2014
comment
Спасибо PaulR. Я не знал, что _mm256_broadcast_pd() тоже может загружаться из памяти (я думал, что он может читать только из регистра XMM) - person netvope; 14.05.2014
comment
Кстати, в этом случае и _mm256_permute_pd, и _mm256_shuffle_pd компилируются в vunpcklpd/vunpckhpd. - person netvope; 14.05.2014