Заставить встроенные функции AVX использовать вместо этого инструкции SSE

К сожалению, у меня есть процессор AMD piledriver, у которого, похоже, проблемы с инструкциями AVX:

Запись в память с 256-битными регистрами AVX исключительно медленная. Измеренная пропускная способность в 5-6 раз ниже, чем у предыдущей модели (Bulldozer), и в 8-9 раз медленнее, чем две 128-битные записи.

По моему собственному опыту, я обнаружил, что встроенные функции mm256 намного медленнее, чем mm128, и я предполагаю, что это по вышеуказанной причине.

Я действительно хочу кодировать для новейшего набора инструкций AVX, но при этом иметь возможность тестировать сборки на моей машине с разумной скоростью. Есть ли способ заставить встроенные функции mm256 использовать вместо этого инструкции SSE? Я использую VS 2015.

Если нет легкого пути, как насчет трудного. Заменить <immintrin.h> на заказной заголовок, содержащий мои собственные определения для встроенных функций, которые можно закодировать для использования SSE? Не уверен, насколько это правдоподобно, предпочитайте более простой способ, если это возможно, прежде чем я выполню эту работу.


person Thomas    schedule 01.11.2015    source источник
comment
Я не думаю, что есть. Они не собираются переделывать свой компилятор для одного конкретного процессора. (Эта ошибка есть только у Piledriver.)   -  person Mysticial    schedule 01.11.2015
comment
Вы должны давать ссылку, когда что-то цитируете. И да, для этого есть решение. Векторный класс Агнера Фога. Используйте вектор AVX, например Vec8f, и скомпилируйте с -D__SSE4_2__ -D__XOP__.   -  person Z boson    schedule 01.11.2015
comment
Вы используете 256-битный, действительно медленнее? Может у вас проблема с выравниванием?   -  person Cory Nelson    schedule 04.11.2015


Ответы (2)


Воспользуйтесь библиотекой векторных классов Agner Fog и добавьте это в командную строку в Visual Studio: -D__SSE4Vec8f_ -D__XOP__.

Затем используйте вектор размера AVX, например Vec8f для восьми чисел с плавающей запятой. Когда вы компилируете без включения AVX, он будет использовать файл vectorf256e.h, который имитирует AVX с двумя регистрами SSE. Например, Vec8f наследуется от Vec256fe, которое начинается так:

class Vec256fe {
protected:
    __m128 y0;                         // low half
    __m128 y1;                         // high half

Если вы компилируете с /arch:AVX -D__XOP__, VCL вместо этого будет использовать файл vectorf256.h и один регистр AVX. Тогда ваш код работает для AVX и SSE с изменением только переключателя компилятора.

Если вы не хотите использовать XOP, не используйте -D__XOP__.


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

Vec8f a;
Vec4f lo = a.get_low();  // a is a Vec8f type
Vec4f hi = a.get_high();
lo.store(&b[0]);         // b is a float array
hi.store(&b[4]);

затем скомпилируйте с /arch:AVX -D__XOP__.

Другой вариант - один исходный файл, который использует Vecnf, а затем выполняет

//foo.cpp
#include "vectorclass.h"
#if SIMDWIDTH == 4
typedef Vec4f Vecnf;
#else
typedef Vec8f Vecnf;
#endif  

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

cl /O2 /DSIMDWIDTH=4                     foo.cpp /Fofoo_sse
cl /O2 /DSIMDWIDTH=4 /arch:AVX /D__XOP__ foo.cpp /Fofoo_avx128
cl /O2 /DSIMDWIDTH=8 /arch:AVX           foo.cpp /Fofoo_avx256

Это создаст три исполняемых файла с одним исходным файлом. Вместо того, чтобы связывать их, вы можете просто скомпилировать их с /c, и они сделают диспетчер ЦП. Я использовал XOP с avx128, потому что не думаю, что есть веские причины использовать avx128, кроме как на AMD.

person Z boson    schedule 01.11.2015
comment
Интересно, как я мог сделать наоборот. Вставьте __m256 в Vec8fe в библиотеке векторных классов. Да, в этом нет смысла, но мне нужен этот чехол. - person Royi; 19.02.2018
comment
@Royii зачем тебе этот чехол? Если у вас есть __m256, это означает, что вы скомпилировали с включенным AVX, и тогда VCL будет использовать Vec8f, а не Vec8fe. - person Z boson; 19.02.2018
comment
Потому что в некоторых случаях мне нужны 2 разных кода в моей системе. Один для SSE и один для AVX. Проблема с VCL заключается в том, что он обрабатывает только один из них. Я хотел бы заставить его использовать AVX с Vec8f и SSE с Vec4f. - person Royi; 19.02.2018
comment
@Royi, сделай диспетчер ЦП и выбери путь кода исходя из набора инструкций. Использование Vec8fe в некоторых случаях может дать худшие результаты, чем использование Vec4f дважды. Я избегаю эмулируемых типов. - person Z boson; 19.02.2018
comment
Проблема в том, что я не могу этого сделать в своей программе. У меня есть функция, которая построена только с использованием SSE Intrinsics, и такая же функция с использованием AVX Intrinsics. Решение о том, как компилировать, не зависит от компилятора. Я хочу, чтобы это было написано именно так. - person Royi; 19.02.2018
comment
@Royi, задай вопрос по SO. С этими комментариями сложно понять проблему. Внутренний вопрос VCL + интересен. Я не единственный, кто использует VCL, чтобы другие все равно могли ответить на ваш вопрос. - person Z boson; 19.02.2018

Вы не хотите использовать инструкции SSE. Вы хотите, чтобы хранилища по 256 байт выполнялись как два отдельных хранилища по 128 байт, но с инструкциями 128 байт, закодированными в кодировке VEX. т.е. 128b AVX vmovups.


gcc имеет параметры -mavx256-split-unaligned-load и ...-store (включены как часть -march=sandybridge, например, предположительно также для семейства Bulldozer (-march=bdver2 - piledriver). Это не решает проблему, когда компилятор знает, что память выровнена, хотя.


Вы можете переопределить обычное внутреннее хранилище 256b с помощью макроса, такого как

// maybe enable this for all BD family CPUs?

#if defined(__bdver2) | defined(PILEDRIVER) | defined(SPLIT_256b_STORES)
   #define _mm256_storeu_ps(addr, data) do{ \
      _mm_storeu_ps( ((float*)(addr)) + 0, _mm256_extractf128_ps((data),0)); \
      _mm_storeu_ps( ((float*)(addr)) + 4, _mm256_extractf128_ps((data),1)); \
   }while(0)
#endif

gcc определяет __bdver2 (Bulldozer версии 2) для Piledriver (-march=bdver2).

Вы можете сделать то же самое для (выровненного) _mm256_store_ps или просто всегда использовать невыровненный внутренний.

Компиляторы оптимизируют _mm256_extractf128(data,0) до простого преобразования. Т.е. он должен просто скомпилироваться в

vmovups       [rdi], xmm0         ; if data is in xmm0 and addr is in rdi
vextractf128  [rdi+16], xmm0, 1

Однако тестирование на godbolt показывает, что gcc и clang не работают, а затем извлекаются в регистр и затем магазин. ICC правильно генерирует последовательность из двух инструкций.

person Peter Cordes    schedule 04.11.2015
comment
Поскольку на AMD AVX в основном эмулируется аппаратно как SSE дважды, что плохого в использовании инструкций, не закодированных в VEX? Единственное преимущество, которое я могу придумать при использовании инструкций AVX, но при разделении загрузок / сохранений, - это использование меньшего количества регистров и меньшего количества инструкций в кэше инструкций. - person Z boson; 04.11.2015
comment
Я предполагаю, что, поскольку невыровненные нагрузки не могут быть свернуты для инструкций, не закодированных в VEX, это одна из причин использовать инструкции, закодированные в VEX. - person Z boson; 04.11.2015
comment
@Zboson: да, судя по тому, что я читал (например, Agner Fog), на AMD обычно практически нет преимуществ в использовании 256-битных векторов. Векторы 128b с инструкциями в кодировке VEX обычно являются лучшим выбором. Этот ответ полезен, чтобы помочь в разработке / отладке программного обеспечения AVX с использованием машины Piledriver для разработки. Вы можете использовать встроенные функции 256b, не наступая на ошибку производительности 256b-store. Таким образом, вы получите примерно такую ​​же скорость на Piledriver, чем если бы вы написали свой код с использованием встроенных функций _mm_* 128b, но, надеюсь, гораздо больше скорости на Intel HW. - person Peter Cordes; 04.11.2015
comment
Что касается хранилищ, которые вы не можете кодировать вручную, например, регистрировать потоки, компилятор автоматически генерирует инструкции хранилища. Не можете их изменить? - person Thomas; 09.11.2015
comment
@Volatile: нет, нельзя. Правильный термин - «утечка», а не «переполнение», чтобы описать, что делает компилятор, когда у него заканчиваются регистры для хранения всех локальных переменных и временных файлов. (т.е. он переносит регистры в стек.) IDK, если он может предположить, что стек выровнен по 32B, поэтому gcc -mavx256-split-unaligned-store может по-прежнему генерировать хранилище с двумя инструкциями. В большинстве случаев разливы будут редкими (например, несколько раз за вызов, а не за итерацию цикла). Поскольку проблема, которую вы пытаетесь избежать, - это всего лишь проблема с производительностью ~ 17 циклов, а не ошибка сегмента, вы, вероятно,. Ok. - person Peter Cordes; 09.11.2015
comment
На самом деле, неужели одно хранилище 256b полностью заполняет блок хранилища, или же 128b и другие хранилища меньшего размера могут работать, в то время как хранилище 256b, рассчитанное на 17 тактов, находится в полете? - person Peter Cordes; 09.11.2015
comment
@Volatile: я только что протестировал, написав функцию, которая использует 17 __m256 переменных. goo.gl/3cwpgz. Он использует их как аккумуляторы для суммы FP, так что утечка здесь находится внутри цикла. gcc выравнивает стек, а затем создает фрейм стека. Кроме того, я только что обнаружил, что операторы для __m256 переменных в GNU C перегружены, поэтому вы можете добавить их с помощью ymm0 += ymm1, чтобы получить инструкцию vaddps. (gcc / clang, но не icc13). Кроме того, gcc выполняет только один vxorps, а затем использует vmovaps для обнуления других регистров, даже с -march=sandybridge, где vxorps работает 4 / такт, vmovaps r,r 3 / такт. - person Peter Cordes; 09.11.2015