我正在研究向量化对程序性能的影响。为此,我编写了以下代码:
#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字节对齐的。
编辑:
首先,我想感谢大家的评论和答案。
我考虑了@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个字节)。更重要的是,由于所需的内存是连续的,预取器肯定会有很大帮助,并提前加载下一个块。
话虽如此,我认为@Mysticial计算的内存带宽过于悲观。
此外,在Intel向量化指南中提到,使用SIMD来改善非常简单的加法程序的性能。因此,似乎我们应该能够在这个非常简单的循环中获得一些性能提升。
编辑2: 再次感谢您的评论。还要感谢@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核心i5 @ 2.5GHz(双核)
mulpd
。然而,即使是对齐版本也使用奇怪的movsd
+movhpd
序列来加载128b。我认为这是针对c
和a
对齐,b
未对齐(在标量介绍后)的情况。我记得在一些旧架构上,2个指令序列有时比movupd
更快。只有目标对齐版本的循环使用movupd
作为一个源,另一个源使用2个指令方法,/boggle。 - Peter CordesLEN
大小? - Manolete