L oop развертывание и SSE - clang vs gcc - PullRequest
3 голосов
/ 06 января 2020

Отказ от ответственности: полный код можно найти здесь .

16-байтовое выравнивание

Учитывая довольно простой тип для поддержки правильного выравнивания SSE

struct alignas(16) simd_pack
{
    std::int32_t data[4];
};

и функция, которая добавляет два массива вместе

void add_packed(simd_pack* lhs_and_result, simd_pack* rhs, std::size_t size)
{
    for (std::size_t i = 0; i < size; i++)
        for (std::size_t j = 0; j < 4; j++)
            lhs_and_result[i].data[j] += rhs[i].data[j];
}

компилирует код с помощью clang и g cc, используя -O3.

Clang производит следующее сборка:

add_packed(simd_pack*, simd_pack*, unsigned long):          # @add_packed(simd_pack*, simd_pack*, unsigned long)
        test    rdx, rdx
        je      .LBB0_3
        mov     eax, 12
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
        mov     ecx, dword ptr [rsi + rax - 12]
        add     dword ptr [rdi + rax - 12], ecx
        mov     ecx, dword ptr [rsi + rax - 8]
        add     dword ptr [rdi + rax - 8], ecx
        mov     ecx, dword ptr [rsi + rax - 4]
        add     dword ptr [rdi + rax - 4], ecx
        mov     ecx, dword ptr [rsi + rax]
        add     dword ptr [rdi + rax], ecx
        add     rax, 16
        add     rdx, -1
        jne     .LBB0_2
.LBB0_3:
        ret

Я не очень грамотен в сборке, но мне кажется, что Clang просто разворачивает внутреннюю часть для l oop. Если мы посмотрим на g cc, то получим:

add_packed(simd_pack*, simd_pack*, unsigned long):
        test    rdx, rdx
        je      .L1
        sal     rdx, 4
        xor     eax, eax
.L3:
        movdqa  xmm0, XMMWORD PTR [rdi+rax]
        paddd   xmm0, XMMWORD PTR [rsi+rax]
        movaps  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, rdx
        jne     .L3
.L1:
        ret

, чего я и ожидаю.

64-байтовое выравнивание

Разница становится еще больше (очевидно, ) если мы выровняем go до 64 байтов (что обычно является строкой кэша, если я не ошибаюсь)

struct alignas(64) cache_line
{
    std::int32_t data[16];
};

void add_cache_line(cache_line* lhs_and_result, cache_line* rhs, std::size_t size)
{
    for (std::size_t i = 0; i < size; i++)
        for (std::size_t j = 0; j < 16; j++)
            lhs_and_result[i].data[j] += rhs[i].data[j];
}

Clang просто разворачивается:

add_cache_line(cache_line*, cache_line*, unsigned long):    # @add_cache_line(cache_line*, cache_line*, unsigned long)
        test    rdx, rdx
        je      .LBB1_3
        mov     eax, 60
.LBB1_2:                                # =>This Inner Loop Header: Depth=1
        mov     ecx, dword ptr [rsi + rax - 60]
        add     dword ptr [rdi + rax - 60], ecx
        mov     ecx, dword ptr [rsi + rax - 56]
        add     dword ptr [rdi + rax - 56], ecx
        mov     ecx, dword ptr [rsi + rax - 52]
        add     dword ptr [rdi + rax - 52], ecx
        mov     ecx, dword ptr [rsi + rax - 48]
        add     dword ptr [rdi + rax - 48], ecx
        mov     ecx, dword ptr [rsi + rax - 44]
        add     dword ptr [rdi + rax - 44], ecx
        mov     ecx, dword ptr [rsi + rax - 40]
        add     dword ptr [rdi + rax - 40], ecx
        mov     ecx, dword ptr [rsi + rax - 36]
        add     dword ptr [rdi + rax - 36], ecx
        mov     ecx, dword ptr [rsi + rax - 32]
        add     dword ptr [rdi + rax - 32], ecx
        mov     ecx, dword ptr [rsi + rax - 28]
        add     dword ptr [rdi + rax - 28], ecx
        mov     ecx, dword ptr [rsi + rax - 24]
        add     dword ptr [rdi + rax - 24], ecx
        mov     ecx, dword ptr [rsi + rax - 20]
        add     dword ptr [rdi + rax - 20], ecx
        mov     ecx, dword ptr [rsi + rax - 16]
        add     dword ptr [rdi + rax - 16], ecx
        mov     ecx, dword ptr [rsi + rax - 12]
        add     dword ptr [rdi + rax - 12], ecx
        mov     ecx, dword ptr [rsi + rax - 8]
        add     dword ptr [rdi + rax - 8], ecx
        mov     ecx, dword ptr [rsi + rax - 4]
        add     dword ptr [rdi + rax - 4], ecx
        mov     ecx, dword ptr [rsi + rax]
        add     dword ptr [rdi + rax], ecx
        add     rax, 64
        add     rdx, -1
        jne     .LBB1_2
.LBB1_3:
        ret

пока g cc использует SSE и также развертывает:

add_cache_line(cache_line*, cache_line*, unsigned long):
        mov     rcx, rdx
        test    rdx, rdx
        je      .L9
        sal     rcx, 6
        mov     rax, rdi
        mov     rdx, rsi
        add     rcx, rdi
.L11:
        movdqa  xmm2, XMMWORD PTR [rdx+16]
        movdqa  xmm3, XMMWORD PTR [rax]
        add     rax, 64
        add     rdx, 64
        movdqa  xmm1, XMMWORD PTR [rdx-32]
        movdqa  xmm0, XMMWORD PTR [rdx-16]
        paddd   xmm3, XMMWORD PTR [rdx-64]
        paddd   xmm2, XMMWORD PTR [rax-48]
        paddd   xmm1, XMMWORD PTR [rax-32]
        paddd   xmm0, XMMWORD PTR [rax-16]
        movaps  XMMWORD PTR [rax-64], xmm3
        movaps  XMMWORD PTR [rax-48], xmm2
        movaps  XMMWORD PTR [rax-32], xmm1
        movaps  XMMWORD PTR [rax-16], xmm0
        cmp     rax, rcx
        jne     .L11
.L9:
        ret

Без выравнивания

Интересно, если мы используем простые 32-битные целочисленные массивы без выравнивания вообще. Мы используем точно такие же флаги компилятора.

void add_unaligned(std::int32_t* lhs_and_result, std::int32_t* rhs, std::size_t size)
{
    for (std::size_t i = 0; i < size; i++)
        lhs_and_result[i] += rhs[i];
}

Clang

Сборка Clang взорвалась честно, добавив несколько веток:

add_unaligned(int*, int*, unsigned long):                 # @add_unaligned(int*, int*, unsigned long)
        test    rdx, rdx
        je      .LBB2_16
        cmp     rdx, 7
        jbe     .LBB2_2
        lea     rax, [rsi + 4*rdx]
        cmp     rax, rdi
        jbe     .LBB2_9
        lea     rax, [rdi + 4*rdx]
        cmp     rax, rsi
        jbe     .LBB2_9
.LBB2_2:
        xor     r10d, r10d
.LBB2_3:
        mov     r8, r10
        not     r8
        add     r8, rdx
        mov     rcx, rdx
        and     rcx, 3
        je      .LBB2_5
.LBB2_4:                                # =>This Inner Loop Header: Depth=1
        mov     eax, dword ptr [rsi + 4*r10]
        add     dword ptr [rdi + 4*r10], eax
        add     r10, 1
        add     rcx, -1
        jne     .LBB2_4
.LBB2_5:
        cmp     r8, 3
        jb      .LBB2_16
.LBB2_6:                                # =>This Inner Loop Header: Depth=1
        mov     eax, dword ptr [rsi + 4*r10]
        add     dword ptr [rdi + 4*r10], eax
        mov     eax, dword ptr [rsi + 4*r10 + 4]
        add     dword ptr [rdi + 4*r10 + 4], eax
        mov     eax, dword ptr [rsi + 4*r10 + 8]
        add     dword ptr [rdi + 4*r10 + 8], eax
        mov     eax, dword ptr [rsi + 4*r10 + 12]
        add     dword ptr [rdi + 4*r10 + 12], eax
        add     r10, 4
        cmp     rdx, r10
        jne     .LBB2_6
        jmp     .LBB2_16
.LBB2_9:
        mov     r10, rdx
        and     r10, -8
        lea     rax, [r10 - 8]
        mov     r9, rax
        shr     r9, 3
        add     r9, 1
        mov     r8d, r9d
        and     r8d, 1
        test    rax, rax
        je      .LBB2_10
        sub     r9, r8
        xor     ecx, ecx
.LBB2_12:                               # =>This Inner Loop Header: Depth=1
        movdqu  xmm0, xmmword ptr [rsi + 4*rcx]
        movdqu  xmm1, xmmword ptr [rsi + 4*rcx + 16]
        movdqu  xmm2, xmmword ptr [rdi + 4*rcx]
        paddd   xmm2, xmm0
        movdqu  xmm0, xmmword ptr [rdi + 4*rcx + 16]
        paddd   xmm0, xmm1
        movdqu  xmm1, xmmword ptr [rdi + 4*rcx + 32]
        movdqu  xmm3, xmmword ptr [rdi + 4*rcx + 48]
        movdqu  xmmword ptr [rdi + 4*rcx], xmm2
        movdqu  xmmword ptr [rdi + 4*rcx + 16], xmm0
        movdqu  xmm0, xmmword ptr [rsi + 4*rcx + 32]
        paddd   xmm0, xmm1
        movdqu  xmm1, xmmword ptr [rsi + 4*rcx + 48]
        paddd   xmm1, xmm3
        movdqu  xmmword ptr [rdi + 4*rcx + 32], xmm0
        movdqu  xmmword ptr [rdi + 4*rcx + 48], xmm1
        add     rcx, 16
        add     r9, -2
        jne     .LBB2_12
        test    r8, r8
        je      .LBB2_15
.LBB2_14:
        movdqu  xmm0, xmmword ptr [rsi + 4*rcx]
        movdqu  xmm1, xmmword ptr [rsi + 4*rcx + 16]
        movdqu  xmm2, xmmword ptr [rdi + 4*rcx]
        paddd   xmm2, xmm0
        movdqu  xmm0, xmmword ptr [rdi + 4*rcx + 16]
        paddd   xmm0, xmm1
        movdqu  xmmword ptr [rdi + 4*rcx], xmm2
        movdqu  xmmword ptr [rdi + 4*rcx + 16], xmm0
.LBB2_15:
        cmp     r10, rdx
        jne     .LBB2_3
.LBB2_16:
        ret
.LBB2_10:
        xor     ecx, ecx
        test    r8, r8
        jne     .LBB2_14
        jmp     .LBB2_15

Что происходит в .LBB2_4 а .LBB2_6? Похоже, он снова развертывает все oop, но я не уверен, что там происходит (в основном из-за используемых регистров).

В .LBB2_12 он даже развертывает часть SSE. Я думаю, что он развернут только в два раза, потому что для загрузки каждого операнда нужны два SIMD-регистра, потому что они теперь не выровнены. .LBB2_14 содержит часть SSE без развертывания.

Как здесь поток управления? Я предполагаю, что это должно быть:

  1. продолжайте использовать развернутую часть SSE, пока оставшиеся данные не станут слишком маленькими, чтобы заполнить все регистры (xmm0..3)
  2. , чтобы переключиться на одиночный подготовьте часть SSE и сделайте это один раз, если у нас будет достаточно данных, чтобы заполнить xmm0 (в нашем случае 4 целых числа)
  3. обработать оставшиеся данные (максимум 3 операции, в противном случае это снова будет SSE)

Порядок меток и инструкции по переходу сбивают с толку, что (приблизительно) происходит здесь?

G CC

G cc Сборку немного легче читать:

add_unaligned(int*, int*, unsigned long):
        test    rdx, rdx
        je      .L16
        lea     rcx, [rsi+4]
        mov     rax, rdi
        sub     rax, rcx
        cmp     rax, 8
        jbe     .L22
        lea     rax, [rdx-1]
        cmp     rax, 2
        jbe     .L22
        mov     rcx, rdx
        xor     eax, eax
        shr     rcx, 2
        sal     rcx, 4
.L19:
        movdqu  xmm0, XMMWORD PTR [rdi+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddd   xmm0, xmm1
        movups  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, rcx
        jne     .L19
        mov     rax, rdx
        and     rax, -4
        test    dl, 3
        je      .L16
        mov     ecx, DWORD PTR [rsi+rax*4]
        add     DWORD PTR [rdi+rax*4], ecx
        lea     rcx, [rax+1]
        cmp     rdx, rcx
        jbe     .L16
        add     rax, 2
        mov     r8d, DWORD PTR [rsi+rcx*4]
        add     DWORD PTR [rdi+rcx*4], r8d
        cmp     rdx, rax
        jbe     .L16
        mov     edx, DWORD PTR [rsi+rax*4]
        add     DWORD PTR [rdi+rax*4], edx
        ret
.L22:
        xor     eax, eax
.L18:
        mov     ecx, DWORD PTR [rsi+rax*4]
        add     DWORD PTR [rdi+rax*4], ecx
        add     rax, 1
        cmp     rdx, rax
        jne     .L18
.L16:
        ret

Я предполагаю, что поток управления похож на clang

  1. Продолжайте использовать одноступенчатую часть SSE, пока оставшиеся данные не станут слишком большими малый для заполнения xmm0 и xmm1
  2. обрабатывают оставшиеся данные (максимум 3 операции, в противном случае это снова подходит для SSE)

Похоже, именно это и происходит в .L19 но что тогда делает .L18

Резюме

Здесь - полный код, включая сборку. Мой вопрос:

  1. Почему Clang развертывает функции, которые используют выровненные данные вместо SSE или их комбинации (например, g cc)?
  2. Что такое .LBB2_4 и .LBB2_6 в сборке clang?
  3. Верны ли мои предположения о потоке управления функцией с невыровненными данными?
  4. Что такое .L18 в g cc s сборка делать?
...