C 为什么向量化循环没有提高性能
我正在调查矢量化对程序性能的影响。在这方面,我编写了以下代码:C 为什么向量化循环没有提高性能,c,performance,simd,icc,C,Performance,Simd,Icc,我正在调查矢量化对程序性能的影响。在这方面,我编写了以下代码: #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 = (
#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;
}
我使用以下两个命令编译代码:
1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2
我希望看到性能的提高,因为第二个命令成功地将循环矢量化。然而,我的研究表明,当循环矢量化时,性能并没有提高
我可能错过了一些东西,因为我对这个话题不太熟悉。所以,如果我的代码有问题,请告诉我
提前感谢你的帮助
PS:我使用的是MacOSX,所以不需要对齐数据,因为所有分配的内存都是16字节对齐的
编辑:
首先,我要感谢大家的评论和回答。
我考虑了@Mystical提出的答案,这里还有一些需要进一步提及的问题。
首先,正如@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
所需的内存是连续的,因此预取器肯定会有很大帮助,并提前加载下一个块。
话虽如此,我认为@Mystical计算的内存带宽太悲观了
此外,本文还提到了使用SIMD来提高程序的性能,只需进行一个非常简单的加法。因此,对于这个非常简单的循环,我们似乎应该能够获得一些性能改进
编辑2:
再次感谢您的评论。另外,感谢@Mysticial示例代码,我终于看到了SIMD对性能改进的影响。正如Mystical提到的,问题在于内存带宽。通过为适合一级缓存的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
编译时,我的处理器只需要一个周期就可以完成一次迭代
注:我的电脑是Macbook Pro core i5@2.5GHz(双核)这一原始答案在2013年有效。截至2017年,情况发生了很大变化,问题和答案都过时了 2017年更新见本答案末尾
原始答案(2013): 因为内存带宽限制了你 虽然矢量化和其他微观优化可以提高计算速度,但它们不能提高内存的速度 在您的示例中:
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
- 处理器:英特尔Core i7 2600K@4.2 GHz
- 编译器:Visual Studio 2012
- 时间:6.55秒
=27510000000个周期6.55*4.2 GHz
=1.074周期/迭代27510000000/25600000000
现在,如果您想知道如何做到:
- 2负载
- 1商店
- 1乘
- 增量计数器
- 比较+分支
内存带宽约为10 GB/s: 最简单的测试方法是通过
memset()
:
#包括
#包括
使用std::cout;
使用std::endl;
int main(){
常数int LEN=1 Y
X
。但是X*(#核心)>Y
- 无矢量化(8字节加载/存储):
和X=32 GB/s
Y=~17 GB/s
- 矢量化SSE*(16字节加载/存储):
和X=64 GB/s
Y=~17 GB/s
- 无矢量化(8字节加载/存储):
和X=32 GB/s
Y=~70 GB/s
- 矢量化AVX*(32字节加载/存储):
和X=64 GB/s
Y=~70 GB/s
X
的限制。但您仍然会达到主内存带宽限制
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
#include <iostream>
#include <time.h>
using std::cout;
using std::endl;
int main(){
const int LEN = 256;
double *a = (double*)malloc(LEN*sizeof(*a));
double *b = (double*)malloc(LEN*sizeof(*a));
double *c = (double*)malloc(LEN*sizeof(*a));
int k;
for(k = 0; k < LEN; k++){
a[k] = rand();
b[k] = rand();
}
clock_t time0 = clock();
for (int i = 0; i < 100000000; i++){
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
}
clock_t time1 = clock();
cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl;
}
#include <iostream>
#include <time.h>
using std::cout;
using std::endl;
int main(){
const int LEN = 1 << 30; // 1GB
char *a = (char*)calloc(LEN,1);
clock_t time0 = clock();
for (int i = 0; i < 100; i++){
memset(a,0xff,LEN);
}
clock_t time1 = clock();
cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl;
}
#include <string.h> /* for memcpy */
...
gettimeofday(&stTime, NULL);
for(k = 0; k < LEN; k += 4) {
double a4[4], b4[4], c4[4];
memcpy(a4,a+k, sizeof a4);
memcpy(b4,b+k, sizeof b4);
c4[0] = a4[0] * b4[0];
c4[1] = a4[1] * b4[1];
c4[2] = a4[2] * b4[2];
c4[3] = a4[3] * b4[3];
memcpy(c+k,c4, sizeof c4);
}
gettimeofday(&endTime, NULL);