Объединение __restrict__ и __attribute __ ((выровнено (32))) - PullRequest
0 голосов
/ 15 января 2019

Я хочу убедиться, что gcc знает:

  1. Указатели относятся к неперекрывающимся частям памяти
  2. Указатели имеют 32 байтовых выравнивания

Правильно ли следующее?

template<typename T, typename T2>
void f(const  T* __restrict__ __attribute__((aligned(32))) x,
       T2* __restrict__ __attribute__((aligned(32))) out) {}

Спасибо.

Обновление:

Я пытаюсь использовать одно чтение и много записи для насыщения портов процессора для записи. Я надеюсь, что это повысит производительность за счет выровненных ходов.

Но сборка по-прежнему использует невыровненные ходы вместо выровненных ходов.

Код (также на godbolt.org )

int square(const  float* __restrict__ __attribute__((aligned(32))) x,
           const int size,
           float* __restrict__ __attribute__((aligned(32))) out0,
           float* __restrict__ __attribute__((aligned(32))) out1,
           float* __restrict__ __attribute__((aligned(32))) out2,
           float* __restrict__ __attribute__((aligned(32))) out3,
           float* __restrict__ __attribute__((aligned(32))) out4) {
    for (int i = 0; i < size; ++i) {
        out0[i] = x[i];
        out1[i] = x[i] * x[i];
        out2[i] = x[i] * x[i] * x[i];
        out3[i] = x[i] * x[i] * x[i] * x[i];
        out4[i] = x[i] * x[i] * x[i] * x[i] * x[i];
    }
}

Сборка скомпилирована с gcc 8.2 и "-march = haswell -O3" Он полон vmovups, которые являются движениями без выравнивания.

.L3:
        vmovups ymm1, YMMWORD PTR [rbx+rax]
        vmulps  ymm0, ymm1, ymm1
        vmovups YMMWORD PTR [r14+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [r15+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [r12+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [rbp+0+rax], ymm0
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        and     r13d, -8
        vzeroupper

Такое же поведение даже для песчаного моста:

.L3:
        vmovups xmm2, XMMWORD PTR [rbx+rax]
        vinsertf128     ymm1, ymm2, XMMWORD PTR [rbx+16+rax], 0x1
        vmulps  ymm0, ymm1, ymm1
        vmovups XMMWORD PTR [r14+rax], xmm0
        vextractf128    XMMWORD PTR [r14+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [r13+0+rax], xmm0
        vextractf128    XMMWORD PTR [r13+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [r12+rax], xmm0
        vextractf128    XMMWORD PTR [r12+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [rbp+0+rax], xmm0
        vextractf128    XMMWORD PTR [rbp+16+rax], ymm0, 0x1
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        and     r15d, -8
        vzeroupper

Использование сложения вместо умножения ( godbolt ). Все еще не выровненные ходы.

1 Ответ

0 голосов
/ 15 января 2019

Нет, использование float *__attribute__((aligned(32))) x означает, что сам указатель хранится в выровненной памяти, не указывая на выровненную память. 1

Есть способ сделать это, но он помогает только для gcc, а не для clang или ICC.

См. Как сообщить GCC, что аргумент указателя всегда выровнен по двойному слову? для __builtin_assume_aligned, который работает на всех компиляторах, совместимых с GNU C, и Как применить __attribute __ (( выровненный (32))) к int *? для более подробной информации о __attribute__((aligned(32))), который работает для GCC.

Я использовал __restrict вместо __restrict__, поскольку это расширение C ++ для C99 restrict переносимо для всех основных компиляторов x86 C ++, включая MSVC.

typedef float aligned32_float __attribute__((aligned(32)));

void prod(const aligned32_float  * __restrict x,
          const aligned32_float  * __restrict y,
          int size,
          aligned32_float* __restrict out0)
{
    size &= -16ULL;

#if 0   // this works for clang, ICC, and GCC
    x = (const float*)__builtin_assume_aligned(x, 32);  // have to cast the result in C++
    y = (const float*)__builtin_assume_aligned(y, 32);
    out0 = (float*)__builtin_assume_aligned(out0, 32);
#endif

    for (int i = 0; i < size; ++i) {
        out0[i] = x[i] * y[i];  // auto-vectorized with a memory operand for mulps
      // note clang using two separate movups loads
      // instead of a memory operand for mulps
    }
}

( вывод gcc, clang и ICC в проводнике компилятора Godbolt ).


GCC и clang будут использовать movaps / vmovaps вместо ups каждый раз, когда у них есть гарантия выравнивания во время компиляции. (В отличие от MSVC и ICC, которые никогда не используют movaps для загрузки / хранения, пропущенная оптимизация для всего, что работает на Core2 / K10 или старше). И, как вы заметили, он применяет эффекты -mavx256-split-unaligned-load / store к настройкам, отличным от Haswell ( Почему gcc не разрешает _mm256_loadu_pd как один vmovupd? ). Это еще одна подсказка, которую не использовал ваш синтаксис работа.

vmovups не является проблемой производительности при использовании в выровненной памяти; он работает идентично vmovaps на всех процессорах, поддерживающих AVX, когда адрес выровнен во время выполнения. Так что на практике нет реальной проблемы с вашим -march=haswell выводом. Только старые процессоры, до Nehalem и Bulldozer, всегда декодировали movups на несколько мопов.

Реальным преимуществом (в наши дни) рассказа компилятору о гарантиях выравнивания является то, что компиляторы иногда генерируют дополнительный код для циклов запуска / очистки, чтобы достичь границы выравнивания. Или без AVX компиляторы не могут сложить загрузку в операнд памяти за mulps, если он не выровнен.

Хорошим примером для этого является out0[i] = x[i] * y[i], где результат загрузки требуется только один раз. или out0[i] *= x[i]. Знание выравнивания позволяет movaps / mulps xmm0, [rsi], иначе это 2x movups + mulps. Вы можете проверить эту оптимизацию даже на таких компиляторах, как ICC или MSVC, которые используют movups, даже если они do знают, что у них есть гарантия выравнивания, но они все равно будут создавать код, необходимый для выравнивания, когда они могут сложить загрузить в операцию ALU.

Кажется, __builtin_assume_aligned - единственный действительно переносимый (для компиляторов GNU C) способ сделать это . Вы можете делать хаки, такие как передача указателей на struct aligned_floats { alignas(32) float f[8]; };, но это просто громоздко в использовании, и если вы не получите доступ к памяти через объекты этого типа, компиляторы не получат выравнивание. (например, приведение указателя обратно к float *


Я пытаюсь использовать одно чтение и много записи для насыщения портов процессора для записи.

Использование более 4 выходных потоков может повредить, что приведет к большему количеству пропусков конфликтов в кеше. Кеш L2 Skylake, например, только 4-сторонний. Но L1d 8-полосный, так что вы, вероятно, в порядке для небольших буферов.

Если вы хотите насытить пропускную способность порта хранилища, используйте более узкие хранилища (например, скалярные), а не широкие хранилища SIMD, для которых требуется больше пропускной способности на моп. Резервные хранилища в одной и той же строке кэша могут быть объединены в буфере хранилища перед фиксацией в L1d, поэтому это зависит от того, что вы хотите протестировать.

Полусвязанный: шаблон доступа к памяти в 2x нагрузке + 1x, такой как c[i] = a[i]+b[i] или триада STREAM, будет ближе всего к максимальной загрузке кэша L1d + пропускной способности хранилища на процессорах семейства Intel Sandybridge. В SnB / IvB 256-битные векторы занимают 2 цикла на загрузку / хранилище, оставляя время для хранения адресов адресов для использования AGU на портах 2 или 3 во время 2-го цикла загрузки. В Haswell и более поздних версиях (порты загрузки / хранения 256-битной ширины) хранилищам необходимо использовать неиндексированный режим адресации, чтобы они могли использовать AGU хранилища в режиме простой адресации на порту 7.

Но процессоры AMD могут выполнять до 2 операций с памятью за такт, при этом самое большее один из них является хранилищем, поэтому они будут максимально использовать шаблон «копировать и работать» = загружать.

Кстати, Intel недавно анонсировала Sunny Cove (преемник Ice Lake), который будет иметь 2x нагрузка + 2x пропускная способность магазина за такт, ALU 2-го вектора в случайном порядке и выпуск / переименование шириной 5. Так что это весело! Компиляторам потребуется развернуть циклы как минимум на 2, чтобы не создавать узких мест в ветвях циклов 1 на такт.


Сноска 1 : Вот почему (если вы компилируете без AVX), вы получаете предупреждение, а gcc пропускает and rsp,-32, поскольку предполагает, что RSP уже выровнен. (На самом деле он не проливает никаких регистров YMM, так что в любом случае следовало бы оптимизировать это, но у gcc некоторое время была ошибка с пропущенной оптимизацией с локальными объектами или объектами, созданными автоматическими векторизациями с дополнительным выравниванием.)

<source>:4:6: note: The ABI for passing parameters with 32-byte alignment has changed in GCC 4.6
...