сумма перекрывающихся массивов, авто-векторизация и ограничение

У Арстечни недавно была статья Почему некоторые языки программирования быстрее других. Он сравнивает Fortran и C и упоминает массивы суммирования. В Фортране предполагается, что массивы не перекрываются, что позволяет проводить дальнейшую оптимизацию. В C / C ++ указатели на один и тот же тип могут перекрываться, поэтому эту оптимизацию нельзя использовать вообще. Однако в C / C ++ можно использовать restrict или же __restrict ключевое слово, указывающее компилятору не предполагать, что указатели перекрываются. Так что я начал изучать это в отношении авто-векторизации.

Следующий код векторизован в GCC и MSVC

void dot_int(int *a, int *b, int *c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}

Я проверил это с перекрывающимися массивами и без них, и он получил правильный результат. Однако способ, которым я бы векторизовал этот цикл вручную с помощью SSE, не обрабатывает перекрывающиеся массивы.

int i=0;
for(; i<n-3; i+=4) {
__m128i a4 = _mm_loadu_si128((__m128i*)&a[i]);
__m128i b4 = _mm_loadu_si128((__m128i*)&b[i]);
__m128i c4 = _mm_add_epi32(a4,b4);
_mm_storeu_si128((__m128i*)c, c4);
}
for(; i<n; i++) {
c[i] = a[i] + b[i];
}

Затем я попытался с помощью __restrict, Я предположил, что, поскольку компилятор может предполагать, что массивы не перекрываются, он не будет обрабатывать перекрывающиеся массивы, но и GCC, и MSVC все еще получают правильный результат для перекрывающихся массивов, даже если __restrict,

void dot_int_restrict(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}

Почему автоматически векторизованный код с и без __restrict получить правильный результат для перекрывающихся массивов?.

Вот полный код, который я использовал для проверки этого:

#include <stdio.h>
#include <immintrin.h>
void dot_int(int *a, int *b, int *c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}

void dot_int_restrict(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}

void dot_int_SSE(int *a, int *b, int *c, int n) {
int i=0;
for(; i<n-3; i+=4) {
__m128i a4 = _mm_loadu_si128((__m128i*)&a[i]);
__m128i b4 = _mm_loadu_si128((__m128i*)&b[i]);
__m128i c4 = _mm_add_epi32(a4,b4);
_mm_storeu_si128((__m128i*)c, c4);
}
for(; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}

int main() {
const int n = 100;
int a[] = {1,1,1,1,1,1,1,1};
int b1[] = {1,1,1,1,1,1,1,1,1};
int b2[] = {1,1,1,1,1,1,1,1,1};
int b3[] = {1,1,1,1,1,1,1,1,1};

int c[8];
int *c1 = &b1[1];
int *c2 = &b2[1];
int *c3 = &b3[1];

dot_int(a,b1,c, 8);
dot_int_SSE(a,b1,c,8);

dot_int(a,b1,c1, 8);
dot_int_restrict(a,b2,c2,8);
dot_int_SSE(a,b3,c3,8);

}

Выход (из MSVC)

2 2 2 2 2 2 2 2 //no overlap default
2 2 2 2 2 2 2 2 //no overlap with manual SSE vector code
2 3 4 5 6 7 8 9 //overlap default
2 3 4 5 6 7 8 9 //overlap with restrict
3 2 2 2 1 1 1 1 //manual SSE vector code

Редактировать:

Вот еще одна версия вставки, которая производит гораздо более простой код

void dot_int(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
a = (int*)__builtin_assume_aligned (a, 16);
b = (int*)__builtin_assume_aligned (b, 16);
c = (int*)__builtin_assume_aligned (c, 16);
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}

9

Решение

Я не понимаю, в чем проблема. Тестирование в Linux / 64 бит, GCC 4.6, -O3, -mtune = native, -msse4.1 (то есть очень старый компилятор / система), этот код

void dot_int(int *a, int *b, int *c, int n) {
for(int i=0; i<n; ++i) {
c[i] = a[i] + b[i];
}
}

компилируется в этот внутренний цикл:

.L4:
movdqu  (%rdi,%rax), %xmm1
addl    $1, %r8d
movdqu  (%rsi,%rax), %xmm0
paddd   %xmm1, %xmm0
movdqu  %xmm0, (%rdx,%rax)
addq    $16, %rax
cmpl    %r8d, %r10d
ja      .L4
cmpl    %r9d, %ecx
je      .L1

Пока этот код

void dot_int_restrict(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
for(int i=0; i<n; ++i) {
c[i] = a[i] + b[i];
}
}

компилируется в это:

.L15:
movdqu  (%rbx,%rax), %xmm0
addl    $1, %r8d
paddd   0(%rbp,%rax), %xmm0
movdqu  %xmm0, (%r11,%rax)
addq    $16, %rax
cmpl    %r10d, %r8d
jb      .L15
addl    %r12d, %r9d
cmpl    %r12d, %r13d
je      .L10

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

Также есть место для дополнительных оптимизаций — GCC не знает, что параметры f.i. Выровненный по 128 битам, следовательно, он должен генерировать огромную преамбулу, чтобы проверить, нет ли проблем с выравниванием (YMMV), и иметь возможность работать с дополнительными не выровненными частями (или менее широкими, чем 128 бит). Это на самом деле происходит с обеими версиями выше. Это полный код, сгенерированный для dot_int:

dot_int:
.LFB626:
.cfi_startproc
testl   %ecx, %ecx
pushq   %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
jle     .L1
leaq    16(%rdx), %r11
movl    %ecx, %r10d
shrl    $2, %r10d
leal    0(,%r10,4), %r9d
testl   %r9d, %r9d
je      .L6
leaq    16(%rdi), %rax
cmpl    $6, %ecx
seta    %r8b
cmpq    %rax, %rdx
seta    %al
cmpq    %r11, %rdi
seta    %bl
orl     %ebx, %eax
andl    %eax, %r8d
leaq    16(%rsi), %rax
cmpq    %rax, %rdx
seta    %al
cmpq    %r11, %rsi
seta    %r11b
orl     %r11d, %eax
testb   %al, %r8b
je      .L6
xorl    %eax, %eax
xorl    %r8d, %r8d
.p2align 4,,10
.p2align 3
.L4:
movdqu  (%rdi,%rax), %xmm1
addl    $1, %r8d
movdqu  (%rsi,%rax), %xmm0
paddd   %xmm1, %xmm0
movdqu  %xmm0, (%rdx,%rax)
addq    $16, %rax
cmpl    %r8d, %r10d
ja      .L4
cmpl    %r9d, %ecx
je      .L1
.L3:
movslq  %r9d, %r8
xorl    %eax, %eax
salq    $2, %r8
addq    %r8, %rdx
addq    %r8, %rdi
addq    %r8, %rsi
.p2align 4,,10
.p2align 3
.L5:
movl    (%rdi,%rax,4), %r8d
addl    (%rsi,%rax,4), %r8d
movl    %r8d, (%rdx,%rax,4)
addq    $1, %rax
leal    (%r9,%rax), %r8d
cmpl    %r8d, %ecx
jg      .L5
.L1:
popq    %rbx
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
xorl    %r9d, %r9d
jmp     .L3
.cfi_endproc

Теперь в вашем случае эффективно не выровнены (как они в стеке), но если вы можете сделать их выровненными и сказать GCC так, то вы можете улучшить генерацию кода:

typedef int intvec __attribute__((vector_size(16)));

void dot_int_restrict_alig(intvec * restrict a,
intvec * restrict b,
intvec * restrict c,
unsigned int n) {
for(unsigned int i=0; i<n; ++i) {
c[i] = a[i] + b[i];
}
}

Это генерирует этот код без преамбулы:

dot_int_restrict_alig:
.LFB628:
.cfi_startproc
testl   %ecx, %ecx
je      .L23
subl    $1, %ecx
xorl    %eax, %eax
addq    $1, %rcx
salq    $4, %rcx
.p2align 4,,10
.p2align 3
.L25:
movdqa  (%rdi,%rax), %xmm0
paddd   (%rsi,%rax), %xmm0
movdqa  %xmm0, (%rdx,%rax)
addq    $16, %rax
cmpq    %rcx, %rax
jne     .L25
.L23:
rep
ret
.cfi_endproc

Обратите внимание на использование совмещенных 128-битных инструкций загрузки (movdqa, как выровнен, против movdquВыровненный)

5

Другие решения

Если вы используете «restrict» с перекрывающимися массивами, вы получите неопределенное поведение. Это то, что вы получаете в случае «перекрытия с ограничением». Неопределенное поведение означает что-нибудь может случиться. И это сделал. По стечению обстоятельств поведение было таким же, как и без «ограничений». Абсолютно правильно. Это подпадает под определение «все может случиться». Нечего жаловаться.

2

Я думаю, что понимаю, что происходит сейчас. Оказывается, что MSVC и GCC дают разные результаты с __restrict, MSVC получает правильный ответ с перекрытием, а GCC — нет. Я думаю, что было бы справедливо сделать вывод, что MSVC игнорирует __restrict Ключевое слово и GCC использует его для дальнейшей оптимизации.

Выход из GCC

2 2 2 2 2 2 2 2 //no overlap default
2 2 2 2 2 2 2 2 //no overlap with manual SSE vector code
2 3 4 5 6 7 8 9 //overlap without __restrict
2 2 2 2 3 2 2 2 //overlap with __restrict
3 2 2 2 1 1 1 1 //manual SSE vector code

Мы можем создать чисто векторизованную функцию, которая дает почти столько же сборочных строк, сколько делает C (весь код генерируется с помощью -O3 в GCC 4.9.0) это:

void dot_int(int * __ restrict a, int * __restrict b, int * __restrict c) {
a = (int*)__builtin_assume_aligned (a, 16);
b = (int*)__builtin_assume_aligned (b, 16);
c = (int*)__builtin_assume_aligned (c, 16);
for(int i=0; i<1024; i++) {
c[i] = a[i] + b[i];
}
}

Производит

dot_int(int*, int*, int*):
xorl    %eax, %eax
.L2:
movdqa  (%rdi,%rax), %xmm0
paddd   (%rsi,%rax), %xmm0
movaps  %xmm0, (%rdx,%rax)
addq    $16, %rax
cmpq    $4096, %rax
jne .L2
rep ret

Однако, если мы удалим __restrict on a который позволяет перекрываться с с я определил, глядя на сборку, которая

void dot_int(int * a, int * __restrict b, int * __restrict c) {
a = (int*)__builtin_assume_aligned (a, 16);
b = (int*)__builtin_assume_aligned (b, 16);
c = (int*)__builtin_assume_aligned (c, 16);
for(int i=0; i<1024; i++) {
c[i] = a[i] + b[i];
}
}

Идентично

__attribute__((optimize("no-tree-vectorize")))
inline void dot_SSE(int * __restrict a, int * __restrict b, int * __restrict c) {
for(int i=0; i<1024; i+=4) {
__m128i a4 = _mm_load_si128((__m128i*)&a[i]);
__m128i b4 = _mm_load_si128((__m128i*)&a[i]);
__m128i c4 = _mm_add_epi32(a4,b4);
_mm_store_si128((__m128i*)&c[i],c4);
}
}
__attribute__((optimize("no-tree-vectorize")))
void dot_int(int * __restrict a, int * __restrict b, int * __restrict c) {
a = (int*)__builtin_assume_aligned (a, 16);
b = (int*)__builtin_assume_aligned (b, 16);
c = (int*)__builtin_assume_aligned (c, 16);
int pass = 1;
if((c+4)<a || (a+4)<c) pass = 0;
if(pass) {
for(int i=0; i<1024; i++) {
c[i] = a[i] + b[i];
}
}
else {
dot_SSE(a,b,c);
}
}

Другими словами, если указатели a и c находятся в пределах 16 байт друг от друга (| a-c |<4) затем код ветвится до не векторизованной формы. Это подтверждает мое предположение, что векторизованный код включает в себя как векторизованную, так и не векторизованную версию для обработки перекрытия.

На перекрывающихся массивах это дает правильный результат: 2 3 4 5 6 7 8 9

2
По вопросам рекламы [email protected]