Подтвердить что ты не робот

Неверное gcc сгенерированное упорядочение сборки приводит к результату

У меня есть следующий код, который копирует данные из памяти в буфер DMA:

for (; likely(l > 0); l-=128)
{
    __m256i m0 = _mm256_load_si256( (__m256i*) (src) );
    __m256i m1 = _mm256_load_si256( (__m256i*) (src+32) );
    __m256i m2 = _mm256_load_si256( (__m256i*) (src+64) );
    __m256i m3 = _mm256_load_si256( (__m256i*) (src+96) );

    _mm256_stream_si256( (__m256i *) (dst), m0 );
    _mm256_stream_si256( (__m256i *) (dst+32), m1 );
    _mm256_stream_si256( (__m256i *) (dst+64), m2 );
    _mm256_stream_si256( (__m256i *) (dst+96), m3 );

    src += 128;
    dst += 128;
}

Вот как выглядит вывод сборки gcc:

405280:       c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
405285:       c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
40528a:       c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
40528f:       c5 fd 6f 18             vmovdqa (%rax),%ymm3
405293:       48 83 e8 80             sub    $0xffffffffffffff80,%rax
405297:       c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
40529c:       c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
4052a1:       c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
4052a6:       c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
4052aa:       48 83 ea 80             sub    $0xffffffffffffff80,%rdx
4052ae:       48 39 c8                cmp    %rcx,%rax
4052b1:       75 cd                   jne    405280 <sender_body+0x6e0>

Обратите внимание на переупорядочение последних инструкций vmovdqa и vmovntdq. С приведенным выше кодом gcc я могу достичь пропускной способности ~ 10 227 571 пакетов в секунду в моем приложении.

Затем я переупорядочу эти инструкции вручную в hexeditor. Это означает, что теперь цикл выглядит следующим образом:

405280:       c5 fd 6f 18             vmovdqa (%rax),%ymm3
405284:       c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
405289:       c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
40528e:       c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
405293:       48 83 e8 80             sub    $0xffffffffffffff80,%rax
405297:       c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
40529b:       c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
4052a0:       c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
4052a5:       c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
4052aa:       48 83 ea 80             sub    $0xffffffffffffff80,%rdx
4052ae:       48 39 c8                cmp    %rcx,%rax
4052b1:       75 cd                   jne    405280 <sender_body+0x6e0>

С правильно заказанными инструкциями я получаю ~ 13 668 313 пакетов в секунду. Таким образом, очевидно, что переупорядочение, введенное gcc, снижает производительность.

Вы сталкивались с этим? Является ли это известной ошибкой или мне нужно заполнить отчет об ошибке?

Флаги компиляции:

-O3 -pipe -g -msse4.1 -mavx

Моя версия gcc:

gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
4b9b3361

Ответ 1

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

  • volatile: Если важно, чтобы обращения к памяти выполнялись в определенном порядке, тогда volatile является подходящим инструментом. Обратите внимание, что это может быть чрезмерным и приведет к отдельной загрузке каждый раз, когда указатель volatile разыменован.

    Нагрузочные/хранилища SSE/AVX нельзя использовать с указателями volatile, поскольку они являются функциями. Используя что-то вроде _mm256_load_si256((volatile __m256i *)src);, неявно отбрасывает его на const __m256i*, теряя квалификатор volatile.

    Мы можем напрямую разыскивать изменчивые указатели. (load/store intrinsics нужны только тогда, когда нам нужно сообщить компилятору, что данные могут быть неровными или что мы хотим хранить потоки).

    m0 = ((volatile __m256i *)src)[0];
    m1 = ((volatile __m256i *)src)[1];
    m2 = ((volatile __m256i *)src)[2];
    m3 = ((volatile __m256i *)src)[3];
    

    К сожалению, это не помогает в магазинах, потому что мы хотим генерировать потоковые магазины. A *(volatile...)dst = tmp; не даст нам то, что мы хотим.

  • __asm__ __volatile__ (""); как барьер переупорядочивания компилятора.

    Это GNU C писал о блокировке памяти компилятора. (Остановка переупорядочения времени компиляции без испускания фактической команды барьера, например mfence). Это останавливает компилятор от переупорядочения доступа к памяти через этот оператор.

  • Использование предела индекса для структур цикла.

    GCC известен очень плохой регистрацией. Более ранние версии сделали много ненужных движений между регистрами, хотя в настоящее время это довольно мало. Тем не менее, тестирование на x86-64 во многих версиях GCC указывает на то, что в циклах лучше использовать индексный предел, а не независимую переменную цикла, для достижения наилучших результатов.

Объединив все вышеизложенное, я построил следующую функцию (после нескольких итераций):

#include <stdlib.h>
#include <immintrin.h>

#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)

void copy(void *const destination, const void *const source, const size_t bytes)
{
    __m256i       *dst = (__m256i *)destination;
    const __m256i *src = (const __m256i *)source;
    const __m256i *end = (const __m256i *)source + bytes / sizeof (__m256i);

    while (likely(src < end)) {
        const __m256i m0 = ((volatile const __m256i *)src)[0];
        const __m256i m1 = ((volatile const __m256i *)src)[1];
        const __m256i m2 = ((volatile const __m256i *)src)[2];
        const __m256i m3 = ((volatile const __m256i *)src)[3];

        _mm256_stream_si256( dst,     m0 );
        _mm256_stream_si256( dst + 1, m1 );
        _mm256_stream_si256( dst + 2, m2 );
        _mm256_stream_si256( dst + 3, m3 );

        __asm__ __volatile__ ("");

        src += 4;
        dst += 4;
    }
}

Компиляция (example.c) с использованием GCC-4.8.4 с использованием

gcc -std=c99 -mavx2 -march=x86-64 -mtune=generic -O2 -S example.c

дает (example.s):

        .file   "example.c"
        .text
        .p2align 4,,15
        .globl  copy
        .type   copy, @function
copy:
.LFB993:
        .cfi_startproc
        andq    $-32, %rdx
        leaq    (%rsi,%rdx), %rcx
        cmpq    %rcx, %rsi
        jnb     .L5
        movq    %rsi, %rax
        movq    %rdi, %rdx
        .p2align 4,,10
        .p2align 3
.L4:
        vmovdqa (%rax), %ymm3
        vmovdqa 32(%rax), %ymm2
        vmovdqa 64(%rax), %ymm1
        vmovdqa 96(%rax), %ymm0
        vmovntdq        %ymm3, (%rdx)
        vmovntdq        %ymm2, 32(%rdx)
        vmovntdq        %ymm1, 64(%rdx)
        vmovntdq        %ymm0, 96(%rdx)
        subq    $-128, %rax
        subq    $-128, %rdx
        cmpq    %rax, %rcx
        ja      .L4
        vzeroupper
.L5:
        ret
        .cfi_endproc
.LFE993:
        .size   copy, .-copy
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

Разборка фактического скомпилированного кода (-c вместо -S)

0000000000000000 <copy>:
   0:   48 83 e2 e0             and    $0xffffffffffffffe0,%rdx
   4:   48 8d 0c 16             lea    (%rsi,%rdx,1),%rcx
   8:   48 39 ce                cmp    %rcx,%rsi
   b:   73 41                   jae    4e <copy+0x4e>
   d:   48 89 f0                mov    %rsi,%rax
  10:   48 89 fa                mov    %rdi,%rdx
  13:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  18:   c5 fd 6f 18             vmovdqa (%rax),%ymm3
  1c:   c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
  21:   c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
  26:   c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
  2b:   c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
  2f:   c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
  34:   c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
  39:   c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
  3e:   48 83 e8 80             sub    $0xffffffffffffff80,%rax
  42:   48 83 ea 80             sub    $0xffffffffffffff80,%rdx
  46:   48 39 c1                cmp    %rax,%rcx
  49:   77 cd                   ja     18 <copy+0x18>
  4b:   c5 f8 77                vzeroupper 
  4e:   c3                      retq

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

Если оптимизировать размер (-Os), код выглядит превосходно на первый взгляд,

0000000000000000 <copy>:
   0:   48 83 e2 e0             and    $0xffffffffffffffe0,%rdx
   4:   48 01 f2                add    %rsi,%rdx
   7:   48 39 d6                cmp    %rdx,%rsi
   a:   73 30                   jae    3c <copy+0x3c>
   c:   c5 fd 6f 1e             vmovdqa (%rsi),%ymm3
  10:   c5 fd 6f 56 20          vmovdqa 0x20(%rsi),%ymm2
  15:   c5 fd 6f 4e 40          vmovdqa 0x40(%rsi),%ymm1
  1a:   c5 fd 6f 46 60          vmovdqa 0x60(%rsi),%ymm0
  1f:   c5 fd e7 1f             vmovntdq %ymm3,(%rdi)
  23:   c5 fd e7 57 20          vmovntdq %ymm2,0x20(%rdi)
  28:   c5 fd e7 4f 40          vmovntdq %ymm1,0x40(%rdi)
  2d:   c5 fd e7 47 60          vmovntdq %ymm0,0x60(%rdi)
  32:   48 83 ee 80             sub    $0xffffffffffffff80,%rsi
  36:   48 83 ef 80             sub    $0xffffffffffffff80,%rdi
  3a:   eb cb                   jmp    7 <copy+0x7>
  3c:   c3                      retq

пока вы не заметите, что последний jmp относится к сравнению, по существу делая jmp, cmp и a jae на каждой итерации, что, вероятно, дает довольно плохие результаты.

Примечание. Если вы делаете что-то похожее для кода реального мира, добавьте комментарии (особенно для __asm__ __volatile__ ("");) и не забудьте периодически проверять все доступные компиляторы, чтобы убедиться, что код не слишком скомпилирован любой.


Глядя на отличный ответ Питера Кордеса, я решил повторить функцию немного дальше, просто для удовольствия.

Как замечает Росс Ридж в комментариях, при использовании _mm256_load_si256() указатель не разыменован (до того, как он будет повторно выбран для выравнивания __m256i * в качестве параметра функции), таким образом volatile не поможет, когда используя _mm256_load_si256(). В другом комментарии Seb предлагает обходное решение: _mm256_load_si256((__m256i []){ *(volatile __m256i *)(src) }), который поставляет функцию указателем на src, обращаясь к элементу с помощью летучего указателя и отбрасывая его в массив. Для простой выровненной нагрузки я предпочитаю прямой испаряемый указатель; он соответствует моему намерению в коде. (Я нацелен на KISS, хотя часто я ударяю только тупую его часть.)

На x86-64 начало внутреннего цикла выравнивается до 16 байтов, поэтому число операций в части "header" функции не очень важно. Тем не менее, избегая избыточного двоичного И (маскируя пять наименее значимых бит суммы, чтобы скопировать в байтах), безусловно, полезно вообще.

GCC предоставляет два варианта для этого. Один из них - это __builtin_assume_aligned(), который позволяет программисту передавать всю информацию о выравнивании компилятору. Другой тип typedefing типа, который имеет дополнительные атрибуты, здесь __attribute__((aligned (32))), который может использоваться, например, для выражения выравнивания параметров функции. Оба они должны быть доступны в clang (хотя поддержка является последней, а не в 3.5 еще), и могут быть доступны в других, таких как icc (хотя ICC, AFAIK, использует __assume_aligned()).

Один из способов смягчить переполнение реестра GCC - это использовать вспомогательную функцию. После некоторых последующих итераций я пришел к этому, another.c:

#include <stdlib.h>
#include <immintrin.h>

#define likely(x)   __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)

#if (__clang_major__+0 >= 3)
#define IS_ALIGNED(x, n) ((void *)(x))
#elif (__GNUC__+0 >= 4)
#define IS_ALIGNED(x, n) __builtin_assume_aligned((x), (n))
#else
#define IS_ALIGNED(x, n) ((void *)(x))
#endif

typedef __m256i __m256i_aligned __attribute__((aligned (32)));


void do_copy(register          __m256i_aligned *dst,
             register volatile __m256i_aligned *src,
             register          __m256i_aligned *end)
{
    do {
        register const __m256i m0 = src[0];
        register const __m256i m1 = src[1];
        register const __m256i m2 = src[2];
        register const __m256i m3 = src[3];

        __asm__ __volatile__ ("");

        _mm256_stream_si256( dst,     m0 );
        _mm256_stream_si256( dst + 1, m1 );
        _mm256_stream_si256( dst + 2, m2 );
        _mm256_stream_si256( dst + 3, m3 );

        __asm__ __volatile__ ("");

        src += 4;
        dst += 4;

    } while (likely(src < end));
}

void copy(void *dst, const void *src, const size_t bytes)
{
    if (bytes < 128)
        return;

    do_copy(IS_ALIGNED(dst, 32),
            IS_ALIGNED(src, 32),
            IS_ALIGNED((void *)((char *)src + bytes), 32));
}

который компилируется с gcc -march=x86-64 -mtune=generic -mavx2 -O2 -S another.c по существу (комментарии и директивы опущены для краткости):

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        vzeroupper
        ret

copy:
        cmpq     $127, %rdx
        ja       .L8
        rep ret
.L8:
        addq     %rsi, %rdx
        jmp      do_copy

Дальнейшая оптимизация в -O3 просто вставляет вспомогательную функцию,

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        vzeroupper
        ret

copy:
        cmpq     $127, %rdx
        ja       .L10
        rep ret
.L10:
        leaq     (%rsi,%rdx), %rax
.L8:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rsi, %rax
        ja       .L8
        vzeroupper
        ret

и даже с -Os сгенерированный код очень приятный,

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        ret

copy:
        cmpq     $127, %rdx
        jbe      .L5
        addq     %rsi, %rdx
        jmp      do_copy
.L5:
        ret

Конечно, без оптимизаций GCC-4.8.4 все еще производит довольно плохой код. При clang-3.5 -march=x86-64 -mtune=generic -mavx2 -O2 и -Os мы получаем существенно

do_copy:
.LBB0_1:
        vmovaps  (%rsi), %ymm0
        vmovaps  32(%rsi), %ymm1
        vmovaps  64(%rsi), %ymm2
        vmovaps  96(%rsi), %ymm3
        vmovntps %ymm0, (%rdi)
        vmovntps %ymm1, 32(%rdi)
        vmovntps %ymm2, 64(%rdi)
        vmovntps %ymm3, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .LBB0_1
        vzeroupper
        retq

copy:
        cmpq     $128, %rdx
        jb       .LBB1_3
        addq     %rsi, %rdx
.LBB1_2:
        vmovaps  (%rsi), %ymm0
        vmovaps  32(%rsi), %ymm1
        vmovaps  64(%rsi), %ymm2
        vmovaps  96(%rsi), %ymm3
        vmovntps %ymm0, (%rdi)
        vmovntps %ymm1, 32(%rdi)
        vmovntps %ymm2, 64(%rdi)
        vmovntps %ymm3, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .LBB1_2
.LBB1_3:
        vzeroupper
        retq

Мне нравится код another.c (он подходит для моего стиля кодирования), и я доволен кодом, созданным GCC-4.8.4 и clang-3.5 в -O1, -O2, -O3, и -Os на обоих, поэтому я думаю, что это достаточно хорошо для меня. (Обратите внимание, однако, что я на самом деле не сравнивал это, потому что у меня нет соответствующего кода. Мы используем как временные, так и невременные (nt) обращения к памяти и поведение кэша (и взаимодействие кеша с окружающим код) имеет первостепенное значение для таких вещей, поэтому я думаю, что это не имеет смысла для микрообнаружения.)

Ответ 2

Прежде всего, обычные люди используют gcc -O3 -march=native -S, а затем редактируют .s для проверки небольших изменений в выходе компилятора. Надеюсь, вам понравилось, что это изменение изменилось.: P Вы также можете использовать Agner Fog отлично objconv, чтобы сделать разборку, которая может быть собрана обратно в двоичный файл с вашим выбором синтаксиса NASM, YASM, MASM или AT & T.


Используя некоторые из тех же идей, что и "Номинальное животное", я сделал версию, которая компилируется с таким же хорошим asm. Я уверен, почему он компилируется на хороший код, хотя, и я догадываюсь, почему порядок имеет значение:

В процессорах имеется несколько (~ 10?) записывающих объединенных буферов для загрузки/хранения NT.

Смотрите эту статью о копировании из видеопамяти с потоковыми нагрузками и записи в основную память с потоковыми хранилищами. Фактически быстрее отказываться от данных через небольшой буфер (намного меньше, чем L1), чтобы избежать загрузки потоковых нагрузок и потоковых хранилищ для буферов заполнения (например, с исполнением вне порядка). Обратите внимание, что использование "потоковой" загрузки NT из обычной памяти не является полезным. Насколько я понимаю, потоковые нагрузки полезны только для ввода-вывода (включая такие вещи, как видеопамять, которая отображается в адресное пространство процессора в области Uncacheable Software-Write-Combining (USWC)). ОЗУ основной памяти отображается WB (Writeback), поэтому CPU разрешено спекулятивно предварительно извлекать его и кэшировать, в отличие от USWC. Во всяком случае, поэтому, хотя я связываю статью об использовании потоковых нагрузок, я не предлагаю использовать потоковые нагрузки. Это просто, чтобы проиллюстрировать, что утверждение для буферов заполнения почти наверняка является причиной того, что gcc-странный код вызывает большую проблему, когда он не будет работать с обычными не-NT-хранилищами.

Также см. комментарий Джона МакАлпина в конце этот поток, поскольку другой источник, подтверждающий, что WC хранит сразу несколько строк кэша, может быть большое замедление.

вывод gcc для вашего исходного кода (по какой-то причине Braindead я не могу себе представить) хранит вторую половину первой строки кэша, затем обе половины второй строки кэша, а затем 1-ю половину первой строки. Вероятно, иногда буфер записи для 1-й линии кэширования сбрасывался до того, как обе половины были написаны, что привело к менее эффективному использованию внешних шин.

clang не делает никакого странного переупорядочения с любой из наших 3 версий (мой, OP и Nominal Animal's).


В любом случае использование ограничений только для компилятора, которые останавливают переупорядочение компилятора, но не выдавать барьерную инструкцию - это один из способов ее остановить. В этом случае это способ попасть в компилятор над головой и сказать "глупый компилятор, не делайте этого". Я не думаю, что вам, как правило, нужно делать это повсюду, но, очевидно, вы не можете доверять gcc с помощью хранилищ с записью (где порядок действительно имеет значение). Поэтому, вероятно, неплохо смотреть на asm, по крайней мере, с помощью компилятора, с которым вы работаете, при использовании загрузок и/или хранилищ NT. Я сообщил об этом для gcc. Ричард Бинер указывает, что -fno-schedule-insns2 - это своего рода метод обхода.

Linux (ядро) уже имеет макрос barrier(), который действует как барьер памяти компилятора. Это почти наверняка только GNU asm volatile(""). За пределами Linux вы можете продолжать использовать расширение GNU, или можете использовать средства C11 stdatomic.h. Они в основном такие же, как объекты С++ 11 std::atomic, с идентичной семантикой AFAIK (слава богу).

Я помещаю барьер между каждым магазином, потому что они свободны, когда в любом случае нет никакого полезного переупорядочения. Оказывается, только один барьер внутри петли сохраняет все в порядке, что и делает ответ Номинального Животного. Это фактически не запрещает компилятору переупорядочивать магазины, у которых нет барьера, разделяющего их; компилятор просто решил не делать этого. Вот почему я препятствовал каждому магазину.


Я только попросил компилятор для барьера записи, потому что я ожидаю, что только заказы NT-хранилища будут важны, а не нагрузки. Вероятно, даже переменные инструкции по загрузке и хранению не будут иметь никакого значения, поскольку все-таки выполнение работ по исполнению трубопроводов все равно. (Обратите внимание, что в статье с текстом Intel-copy-from-video-mem даже использовался mfence, чтобы избежать дублирования между хранилищами потоковой передачи и потоковыми нагрузками.)

atomic_signal_fenceнапрямую не документирует, что с ним делают все различные варианты упорядочивания памяти. Страница С++ для atomic_thread_fence - это одно место на cppreference, где есть примеры и многое другое.

Именно по этой причине я не использовал идею Nominal Animal о объявлении src как указателя на летучесть. gcc решает сохранить нагрузки в том же порядке, что и магазины.


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

Ценные процессоры SnB не могут адаптировать режимы адресации 2-reg для микро-предохранителей, поэтому очевидный способ минимизировать накладные расходы на цикл (получить указатели до конца src и dst, а затем подсчитать отрицательный индекс до нуля) не работает. Магазины не будут микроплавким. Вы бы очень быстро заполнили буферы заполнения до такой степени, что дополнительные шутки все равно не имеют значения. Эта петля, вероятно, не работает около 4 часов за цикл.

Тем не менее, есть способ уменьшить накладные расходы на цикл: с моим нелепым уродливым и нечитаемым в C взломом, чтобы заставить компилятор сделать только один sub (и a cmp/jcc) как накладные расходы на цикл, no разворачивание вообще сделало бы цикл 4-uop, который должен выдаваться на одной итерации за такт даже на SnB. (Обратите внимание, что vmovntdq - это AVX2, а vmovntps - только AVX1. Кланг уже использует vmovaps/vmovntps для si256 intrinsics в этом коде! У них одинаковое требование выравнивания, и все равно, что бит, которые они хранят. Он не сохраняет байты insn, только совместимость.)


Обратитесь к первому абзацу ссылки на ссылку godbolt.

Я предположил, что вы делаете это в ядре Linux, поэтому я вставляю соответствующий #ifdef, поэтому это должно быть правильным, как код ядра или при компиляции для пользовательского пространства.

#include <stdint.h>
#include <immintrin.h>

#ifdef __KERNEL__  // linux has it own macro
//#define compiler_writebarrier()   __asm__ __volatile__ ("")
#define compiler_writebarrier()   barrier()
#else
// Use C11 instead of a GNU extension, for portability to other compilers
#include <stdatomic.h>
// unlike a single store-release, a release barrier is a StoreStore barrier.
// It stops all earlier writes from being delayed past all following stores
// Note that this is still only a compiler barrier, so no SFENCE is emitted,
// even though we're using NT stores.  So from another core perpsective, our
// stores can become globally out of order.
#define compiler_writebarrier()   atomic_signal_fence(memory_order_release)
// this purposely *doesn't* stop load reordering.  
// In this case gcc loads in the same order it stores, regardless.  load ordering prob. makes much less difference
#endif

void copy_pjc(void *const destination, const void *const source, const size_t bytes)
{
          __m256i *dst  = destination;
    const __m256i *src  = source;
    const __m256i *dst_endp = (destination + bytes); // clang 3.7 goes berserk with intro code with this end condition
        // but with gcc it saves an AND compared to Nominal bytes/32:

    // const __m256i *dst_endp = dst + bytes/sizeof(*dst); // force the compiler to mask to a round number


    #ifdef __KERNEL__
    kernel_fpu_begin();  // or preferably higher in the call tree, so lots of calls are inside one pair
    #endif

    // bludgeon the compiler into generating loads with two-register addressing modes like [rdi+reg], and stores to [rdi]
    // saves one sub instruction in the loop.
    //#define ADDRESSING_MODE_HACK
    //intptr_t src_offset_from_dst = (src - dst);
    // generates clunky intro code because gcc can't assume void pointers differ by a multiple of 32

    while (dst < dst_endp)  { 
#ifdef ADDRESSING_MODE_HACK
      __m256i m0 = _mm256_load_si256( (dst + src_offset_from_dst) + 0 );
      __m256i m1 = _mm256_load_si256( (dst + src_offset_from_dst) + 1 );
      __m256i m2 = _mm256_load_si256( (dst + src_offset_from_dst) + 2 );
      __m256i m3 = _mm256_load_si256( (dst + src_offset_from_dst) + 3 );
#else
      __m256i m0 = _mm256_load_si256( src + 0 );
      __m256i m1 = _mm256_load_si256( src + 1 );
      __m256i m2 = _mm256_load_si256( src + 2 );
      __m256i m3 = _mm256_load_si256( src + 3 );
#endif

      _mm256_stream_si256( dst+0, m0 );
      compiler_writebarrier();   // even one barrier is enough to stop gcc 5.3 reordering anything
      _mm256_stream_si256( dst+1, m1 );
      compiler_writebarrier();   // but they're completely free because we are sure this store ordering is already optimal
      _mm256_stream_si256( dst+2, m2 );
      compiler_writebarrier();
      _mm256_stream_si256( dst+3, m3 );
      compiler_writebarrier();

      src += 4;
      dst += 4;
    }

  #ifdef __KERNEL__
  kernel_fpu_end();
  #endif

}

Скомпилируется (gcc 5.3.0 -O3 -march=haswell):

copy_pjc:
        # one insn shorter than Nominal Animal's: doesn't mask the count to a multiple of 32.
        add     rdx, rdi  # dst_endp, destination
        cmp     rdi, rdx  # dst, dst_endp
        jnb     .L7       #,
.L5:
        vmovdqa ymm3, YMMWORD PTR [rsi]   # MEM[base: src_30, offset: 0B], MEM[base: src_30, offset: 0B]
        vmovdqa ymm2, YMMWORD PTR [rsi+32]        # D.26928, MEM[base: src_30, offset: 32B]
        vmovdqa ymm1, YMMWORD PTR [rsi+64]        # D.26928, MEM[base: src_30, offset: 64B]
        vmovdqa ymm0, YMMWORD PTR [rsi+96]        # D.26928, MEM[base: src_30, offset: 96B]
        vmovntdq        YMMWORD PTR [rdi], ymm3 #* dst, MEM[base: src_30, offset: 0B]
        vmovntdq        YMMWORD PTR [rdi+32], ymm2      #, D.26928
        vmovntdq        YMMWORD PTR [rdi+64], ymm1      #, D.26928
        vmovntdq        YMMWORD PTR [rdi+96], ymm0      #, D.26928
        sub     rdi, -128 # dst,
        sub     rsi, -128 # src,
        cmp     rdx, rdi  # dst_endp, dst
        ja      .L5 #,
        vzeroupper
.L7:

Clang делает очень похожий цикл, но intro намного длиннее: clang не предполагает, что src и dest фактически выравниваются. Может быть, он не использует знания о том, что нагрузки и магазины будут виноваты, если не 32B-выровнены? (Он знает, что он может использовать ...aps инструкции вместо ...dqa, поэтому он, безусловно, делает больше оптимизаций для встроенных процессов в стиле компилятора gcc (где они чаще всего всегда превращаются в соответствующую инструкцию). Clang может превратить пару левых/правый вектор смещается в маску из константы, например.)