【发布时间】:2013-08-12 03:20:28
【问题描述】:
我正在研究矢量化对程序性能的影响。对此,我写了如下代码:
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#define LEN 10000000
int main(){
struct timeval stTime, endTime;
double* a = (double*)malloc(LEN*sizeof(*a));
double* b = (double*)malloc(LEN*sizeof(*b));
double* c = (double*)malloc(LEN*sizeof(*c));
int k;
for(k = 0; k < LEN; k++){
a[k] = rand();
b[k] = rand();
}
gettimeofday(&stTime, NULL);
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
gettimeofday(&endTime, NULL);
FILE* fh = fopen("dump", "w");
for(k = 0; k < LEN; k++)
fprintf(fh, "c[%d] = %f\t", k, c[k]);
fclose(fh);
double timeE = (double)(endTime.tv_usec + endTime.tv_sec*1000000 - stTime.tv_usec - stTime.tv_sec*1000000);
printf("Time elapsed: %f\n", timeE);
return 0;
}
在这段代码中,我只是对两个向量进行初始化和相乘。结果保存在矢量c 中。我主要感兴趣的是矢量化以下循环的效果:
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
我使用以下两个命令编译代码:
1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2
我希望看到性能改进,因为第二个命令成功地矢量化了循环。然而,我的研究表明,当循环被矢量化时,性能并没有提升。
我可能在这里遗漏了一些东西,因为我对这个话题不是很熟悉。所以,如果我的代码有问题,请告诉我。
提前感谢您的帮助。
PS:我使用的是 Mac OSX,所以不需要对齐数据,因为所有分配的内存都是 16 字节对齐的。
编辑:
我首先要感谢大家的cmets和答案。
我想到了@Mysticial 提出的答案,这里还有一些应该提到的点。
首先,正如@Vinska 提到的,c[k]=a[k]*b[k] 不仅仅需要一个周期。除了循环索引增量和进行比较以确保k 小于LEN 之外,还需要执行其他操作来执行操作。看一下编译器生成的汇编代码,可以看出一个简单的乘法需要不止一个周期。矢量化版本如下所示:
L_B1.9: # Preds L_B1.8
movq %r13, %rax #25.5
andq $15, %rax #25.5
testl %eax, %eax #25.5
je L_B1.12 # Prob 50% #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.9
testb $7, %al #25.5
jne L_B1.32 # Prob 10% #25.5
# LOE rbx r12 r13 r14 r15
L_B1.11: # Preds L_B1.10
movsd (%r14), %xmm0 #26.16
movl $1, %eax #25.5
mulsd (%r15), %xmm0 #26.23
movsd %xmm0, (%r13) #26.9
# LOE rbx r12 r13 r14 r15 eax
L_B1.12: # Preds L_B1.11 L_B1.9
movl %eax, %edx #25.5
movl %eax, %eax #26.23
negl %edx #25.5
andl $1, %edx #25.5
negl %edx #25.5
addl $10000000, %edx #25.5
lea (%r15,%rax,8), %rcx #26.23
testq $15, %rcx #25.5
je L_B1.16 # Prob 60% #25.5
# LOE rdx rbx r12 r13 r14 r15 eax
L_B1.13: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.14: # Preds L_B1.14 L_B1.13
movups (%r15,%rax,8), %xmm0 #26.23
movsd (%r14,%rax,8), %xmm1 #26.16
movhpd 8(%r14,%rax,8), %xmm1 #26.16
mulpd %xmm0, %xmm1 #26.23
movntpd %xmm1, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.14 # Prob 99% #25.5
jmp L_B1.20 # Prob 100% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.16: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.17: # Preds L_B1.17 L_B1.16
movsd (%r14,%rax,8), %xmm0 #26.16
movhpd 8(%r14,%rax,8), %xmm0 #26.16
mulpd (%r15,%rax,8), %xmm0 #26.23
movntpd %xmm0, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.17 # Prob 99% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.18: # Preds L_B1.17
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.19: # Preds L_B1.18
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.20: # Preds L_B1.14 L_B1.19 L_B1.32
cmpq $10000000, %rdx #25.5
jae L_B1.24 # Prob 0% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.22: # Preds L_B1.20 L_B1.22
movsd (%r14,%rdx,8), %xmm0 #26.16
mulsd (%r15,%rdx,8), %xmm0 #26.23
movsd %xmm0, (%r13,%rdx,8) #26.9
incq %rdx #25.5
cmpq $10000000, %rdx #25.5
jb L_B1.22 # Prob 99% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.24: # Preds L_B1.22 L_B1.20
非矢量化版本是:
L_B1.9: # Preds L_B1.8
xorl %eax, %eax #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.10 L_B1.9
lea (%rax,%rax), %edx #26.9
incl %eax #25.5
cmpl $5000000, %eax #25.5
movsd (%r15,%rdx,8), %xmm0 #26.16
movsd 8(%r15,%rdx,8), %xmm1 #26.16
mulsd (%r13,%rdx,8), %xmm0 #26.23
mulsd 8(%r13,%rdx,8), %xmm1 #26.23
movsd %xmm0, (%rbx,%rdx,8) #26.9
movsd %xmm1, 8(%rbx,%rdx,8) #26.9
jb L_B1.10 # Prob 99% #25.5
# LOE rbx r12 r13 r14 r15 eax
除此之外,处理器不仅仅加载 24 个字节。在每次访问内存时,都会加载一个完整的行(64 字节)。更重要的是,由于a、b 和c 所需的内存是连续的,预取器肯定会有很大帮助并提前加载下一个块。
说了这么多,我觉得@Mysticial计算的内存带宽太悲观了。
此外,Intel Vectorization Guide 中提到了使用 SIMD 来提高程序性能的非常简单的加法。因此,对于这个非常简单的循环,我们似乎应该能够获得一些性能提升。
编辑2:
再次感谢您的 cmets。另外,感谢@Mysticial 示例代码,我终于看到了SIMD 对性能提升的影响。正如 Mysticial 所提到的,问题在于内存带宽。通过为适合L1缓存的a、b和c选择小尺寸,可以看出SIMD有助于显着提高性能。以下是我得到的结果:
icc -O2 -o TestSMIDNoVec -no-vec TestSMID2.c: 17.34 sec
icc -O2 -o TestSMIDVecNoUnroll -vec-report2 TestSMID2.c: 9.33 sec
展开循环可以进一步提高性能:
icc -O2 -o TestSMIDVecUnroll -vec-report2 TestSMID2.c -unroll=8: 8.6sec
另外,我应该提一下,当使用-O2 编译时,我的处理器只需要一个周期即可完成一次迭代。
PS:我的电脑是 Macbook Pro core i5 @2.5GHz(双核)
【问题讨论】:
-
我刚刚更新了我的答案,以证明我的处理器每个周期能够进行 1 次迭代,并解释了它是如何实现的。
-
我真的很讨厌提出这个问题,但是构建命令会将两个版本的可执行文件放在同一个文件中。如果两个版本有不同的名称会更清楚。
-
您说“不需要对齐”,但是生成的 asm 代码会检查所有对齐的可能性。有一个未对齐的 srces 循环,以及一个使用带有内存操作数的
mulpd的循环。然而,即使是对齐版本也使用奇怪的movsd+movhpd序列来加载 128b。我认为这适用于c和a对齐,b未对齐(在标量介绍之后)。我想我记得在一些较旧的架构上读到过,2 insn 序列有时比movupd快。循环的唯一目标对齐版本对一个源使用movupd,对另一个源使用 2 insn 方法,/boggle。 -
LEN你选了什么尺寸的?
标签: c performance simd icc