Почему gcc не разрешает _mm256_loadu_pd как единый vmovupd?

Я пишу какой-то код AVX, и мне нужно загрузить из потенциально не привязанной памяти. В настоящее время я загружаю 4 дубликата , поэтому я бы использовал внутреннюю инструкцию _mm256_loadu_pd ; код, который я написал:

__m256d d1 = _mm256_loadu_pd(vInOut + i*4);

Затем я скомпилировал с параметрами -O3 -mavx -g а затем использовал objdump для получения кода ассемблера плюс аннотированный код и строку ( objdump -S -M intel -l avx.obj ).
Когда я просматриваю базовый код ассемблера, я нахожу следующее:

vmovupd xmm0,XMMWORD PTR [rsi+rax*1]
vinsertf128 ymm0,ymm0,XMMWORD PTR [rsi+rax*1+0x10],0x1

Я ожидал увидеть это:

vmovupd ymm0,XMMWORD PTR [rsi+rax*1]

и полностью использовать 256-битный регистр ( ymm0 ), вместо этого похоже, что gcc решил заполнить 128-битную часть ( xmm0 ), а затем снова загрузить вторую половину с помощью vinsertf128 .

Кто-нибудь может это объяснить?
Эквивалентный код компилируется с помощью одного vmovupd в MSVC VS 2012.

Я запускаю gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 на Ubuntu 18.04 x86-64 .

Всего 2 ответа


Настройка GCC по умолчанию ( -mtune=generic ) включает в себя -mavx256-split-unaligned-load и -mavx256-split-unaligned-store , поскольку это дает небольшое ускорение для некоторых процессоров (например, Sandybridge первого поколения и некоторых процессоров AMD) в в некоторых случаях, когда память фактически смещается во время выполнения.

Используйте -O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store если вы этого не хотите или лучше используете -mtune=haswell . Или используйте -march=native для оптимизации вашего собственного компьютера. Нет настройки «generic-avx2». ( https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html ).

Intel Sandybridge запускает 256-битные нагрузки как один uop, который занимает 2 цикла в порту нагрузки. (В отличие от AMD, которая декодирует все 256-битные векторные инструкции как два отдельных процессора.) У Sandybridge есть проблема с невыровненными 256-битными нагрузками (если адрес фактически смещен во время выполнения). Я не знаю подробностей и не нашел много конкретной информации о том, что такое замедление. Возможно, потому, что он использует кешированный банк, с 16-байтными банками? Но IvyBridge обрабатывает 256-битные нагрузки лучше и все еще имеет кэш-память.

Согласно сообщению списка рассылки GCC о коде, который реализует этот параметр ( https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html ), « он ускоряет некоторые тесты SPEC CPU 2006 до 6% ». (Я думаю, это для Sandybridge, единственного процессора Intel AVX, который существовал в то время.)


Но если на самом деле память на 32 байт выровнена во время выполнения, это чистый минус даже на Sandybridge и большинстве процессоров AMD 1 . Таким образом, с помощью этой опции настройки вы можете просто проиграть, не сообщив своему компилятору о гарантиях выравнивания. И если ваш цикл работает в выровненной памяти большую часть времени, лучше компилировать хотя бы этот блок компиляции с параметрами -mno-avx256-split-unaligned-load или tuning, которые подразумевают это.

Расщепление в программном обеспечении постоянно меняет стоимость. Предоставление аппаратного обеспечения делает его идеально эффективным (за исключением магазинов на Piledriver 1 ), причем смещенный случай может быть медленнее, чем при разнесении программного обеспечения на некоторых процессорах. Таким образом, это пессимистический подход и имеет смысл, если действительно вероятно, что данные действительно смещены во время выполнения, а не просто не гарантируются, что они всегда будут выровнены во время компиляции. например, возможно, у вас есть функция, которая называется большую часть времени с выровненными буферами, но вы все еще хотите, чтобы она работала в редких / маленьких случаях, когда она вызывалась с несогласованными буферами. В этом случае стратегия split-load / store не подходит даже для Sandybridge.

Обычно для буферов должно быть выровнено по 16 байт, но не по 32 байтам, потому что malloc на x86-64 glibc (и new в libstdc ++) возвращает 16-байтовые выровненные буферы (поскольку alignof(maxalign_t) == 16 ). Для больших буферов указатель обычно составляет 16 байт после начала страницы, поэтому он всегда смещается для выравниваний, больших 16. aligned_alloc этого используйте aligned_alloc .


Обратите внимание, что -mavx и -mavx2 вообще не изменяют параметры настройки : gcc -O3 -mavx2 все еще gcc -O3 -mavx2 для всех процессоров, в том числе тех, которые не могут фактически запускать инструкции AVX2. Это довольно глупо, потому что вы должны использовать одну невыровненную 256-битную нагрузку, если настроитесь на «средний AVX2 CPU». К сожалению, gcc не имеет возможности сделать это, а -mavx2 не подразумевает -mno-avx256-split-unaligned-load или что-то еще. См. Https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568 и https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762 для запросов функций, чтобы иметь влияние выбора набора инструкций тюнинг .

Вот почему вы должны использовать -march=native для создания двоичных файлов для локального использования, или, может быть, -march=sandybridge -mtune=haswell для создания двоичных файлов, которые могут запускаться на широком спектре машин, но, вероятно, будут в основном работать на более новом оборудовании, которое AVX. (Обратите внимание, что даже процессоры Skylake Pentium / Celeron не имеют AVX или BMI2, вероятно, на процессорах с любыми дефектами в верхней половине 256-битных исполнительных блоков или файлов регистров они отключают декодирование префиксов VEX и продают их как младшие Pentium.)


Параметры настройки gcc8.2 следующие. ( -march=x означает -mtune=x ). https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html .

Я проверил проводник компилятора Godbolt, выполнив компиляцию с -O3 -fverbose-asm и просмотрев комментарии, которые включают полную дамп всех подразумеваемых опций. Я включил функции _mm256_loadu/storeu_ps и простой цикл с плавающей точкой, который может автоматически зацикливаться, поэтому мы также можем посмотреть, что делает компилятор.

Используйте -mprefer-vector-width=256 (gcc8) или -mno-prefer-avx128 (gcc7 и более ранние версии), чтобы переопределить параметры настройки, такие как -mtune=bdver3 и получить 256-битную -mtune=bdver3 если хотите, а не только с помощью ручного векторизации.

  • default / -mtune=generic : both -mavx256-split-unaligned-load и -store . По-видимому, все меньше и меньше уместно, так как Intel Haswell, а затем стал более распространенным явлением, а недостатком последних процессоров AMD я считаю все еще небольшим. Особенно расщепляются неуравновешенные нагрузки , параметры настройки которых не разрешены.
  • -march=sandybridge и -march=ivybridge : разделите оба. (Я думаю, что я прочитал, что IvyBridge улучшил обработку неустановленных 256-битных загрузок или хранилищ, поэтому он менее подходит для случаев, когда данные могут быть выровнены во время выполнения.)
  • -march=haswell и позже: ни одна опция разделения не включена.
  • -march=knl : не включена опция разделения. (У Silvermont / Atom нет AVX)
  • -mtune=intel : не включена опция разделения. Даже с gcc8, авто-векторизация с -mtune=intel -mavx выбирает для достижения границы выравнивания для массива назначения чтения / записи, в отличие от обычной стратегии gcc8 только с использованием неглавного. (Опять же, еще один случай обработки программного обеспечения, который всегда имеет стоимость, и позволяет оборудованию иметь дело с исключительным случаем.)

  • -march=bdver1 (Bulldozer): -mavx256-split-unaligned-store , но не загружается. Он также устанавливает gcc8-эквивалент gcc7 и более ранний -mprefer-avx128 ( -mprefer-avx128 будет использовать только 128-битный AVX, но, конечно, intrinsics все еще могут использовать 256-битные векторы).
  • -march=bdver2 (Piledriver), bdver3 (Steamroller), bdver4 (Экскаватор). как Бульдозер. Они автоматически процитируют FP цикл a[i] += b[i] с предварительной выборкой программного обеспечения и достаточно разворачиваются только для предварительной выборки по одной строке кэша!
  • -march=znver1 (Zen): -mavx256-split-unaligned-store но не загружает, все еще авто-векторизация всего лишь с 128-битным, но на этот раз без предварительной выборки.
  • -march=btver2 ( AMD Fam16h, aka Jaguar ): ни одна опция разделения не включена, авто-векторизация, как Bulldozer-family, с только 128-битными векторами + предварительная выборка SW.
  • -march=eden-x4 (Via Eden с AVX2): ни одна опция разделения не включена, но опция -march даже не включает -mavx , а авто-векторизация использует 8-байтовые нагрузки movlps / movhps , что действительно глупо. По крайней мере, используйте movsd вместо movlps чтобы сломать ложную зависимость. Но если вы включите -mavx , он использует 128-битные несвязанные нагрузки. Действительно странное / непоследовательное поведение здесь, если для этого нет какого-то странного интерфейса.

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


Сноска 1: у AMD Piledriver есть ошибка производительности, которая делает 256-битную пропускную способность магазина ужасной: даже vmovaps [mem], ymm выравниваются магазины, работающие по одному на 17-20 часов в соответствии с микрогармоном Agner Fog pdf ( https://agner.org/optimize / ). Этот эффект отсутствует в Bulldozer или Steamroller / Excavator.

Agner Fog говорит, что 256-разрядная пропускная способность AVX в целом (не загружается / хранится отдельно) на Bulldozer / Piledriver, как правило, хуже, чем 128-битный AVX, отчасти потому, что он не может декодировать инструкции в шаблоне 2-2 uop. Steamroller делает 256-бит близким к безубыточному (если он не требует дополнительных перетасовки). Но регистр-регистры vmovaps ymm инструкции по-прежнему выигрывают от исключения mov для низких 128 бит на семействе Bulldozer.

Но программное обеспечение с -march=native исходным кодом или двоичные дистрибутивы, как правило, не имеют роскоши построения с -march=native на каждой целевой архитектуре, поэтому есть компромисс при создании двоичного -march=native который может работать на любом AVX-поддерживающем процессоре. Получение большого ускорения с 256-битным кодом на некоторых процессорах обычно стоит того, пока нет никаких катастрофических недостатков на других процессорах.

Разделение неуравновешенных нагрузок / хранилищ - попытка избежать больших проблем на некоторых процессорах. Это связано с дополнительной пропускной способностью uop и дополнительными настройками ALU на последних процессорах. Но, по крайней мере, vinsertf128 ymm, [mem], 1 не нужен блок перетасовки на порту 5 на Haswell / Skylake: он может работать на любом векторном порту ALU. (И это не микро-предохранитель, поэтому он стоит 2-х скоростей пропускной способности интерфейса.)


PS:

Большинство кодов не компилируются компиляторами с кровоточиванием, поэтому изменение «общей» настройки теперь займет некоторое время, прежде чем код, скомпилированный с обновленной настройкой, будет использоваться. (Конечно, большинство кода скомпилировано только с -O2 или -O3 , и эта опция влияет только на код-код AVX. Но многие люди, к сожалению, используют -O3 -mavx2 вместо -O3 -march=native . Поэтому они могут пропустить на FMA, BMI1 / 2, popcnt и другие вещи, поддерживаемые их процессором.


Общая настройка GCC разделяет невыровненные 256-битные нагрузки, чтобы помочь более старым процессорам. (Последующие изменения, по-моему, не позволяют расщеплять нагрузки в универсальной настройке).

Вы можете настроить для более современных процессоров Intel, используя что-то вроде -mtune=intel или -mtune=skylake , и вы получите одну инструкцию, как и предполагалось.


Есть идеи?

10000