重叠数组的和,自动向量化,和限制
sum of overlapping arrays, auto-vectorization, and restrict
Arstechnia最近发表了一篇文章《为什么有些编程语言比其他语言更快》。它比较了Fortran和C,并提到了求和数组。在Fortran中,假设数组不重叠,从而允许进一步优化。在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];
}
}
我不明白是什么问题。在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),并生成一个postable来处理额外的未对齐部分(或小于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
, a对齐,而movdqu
,未对齐)。
如果你对重叠数组使用"restrict",你会得到未定义的行为。这就是"重叠限制"的情况。未定义行为意味着任何都可能发生。它确实做到了。巧合的是,这个行为和没有"限制"是一样的。绝对正确。它完全符合"一切皆有可能"的定义。没什么可抱怨的。
我想我现在明白是怎么回事了。结果表明,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语言一样多(所有代码都是在GCC 4.9.0中使用-O3
生成的):
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
但是,如果我们删除允许a与c重叠的__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
- 新的整数数组向右移动?
- 如何将数组中的元素向右移动?
- C++向数组添加元素并调整数组大小
- 向对象数组 c++ 添加值
- 向动态数组添加内容
- 修复向后数组.size() 无符号整数
- 在C++中向指针数组中的向量添加元素
- 向字符串数组中的元素添加数字值
- 向字符数组 c++ 添加字符
- 在c++中一次向字符串数组中输入一行
- 在C++中向对象数组添加元素
- 向c#数组返回一个字符数组c++
- 向前vs向后数组行走
- 向char数组中添加int类型
- 在c++中有效地向映射数组中添加大量类
- 向动态数组中添加新对象
- 一旦到达行尾,停止向2D数组中输入文本
- 重叠数组的和,自动向量化,和限制
- 向字符数组中添加2个字符
- 向字符串数组中添加随机字符