Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/performance/5.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C 为什么向量化循环没有提高性能_C_Performance_Simd_Icc - Fatal编程技术网

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秒
在这个测试中,我仅在6.55秒内运行了25600000000次迭代

  • 6.55*4.2 GHz
    =27510000000个周期
  • 27510000000/25600000000
    =1.074周期/迭代

现在,如果您想知道如何做到:

  • 2负载
  • 1商店
  • 1乘
  • 增量计数器
  • 比较+分支
全部在一个周期内

这是因为现代处理器和编译器非常棒

虽然每个操作都有延迟(特别是乘法),但处理器能够同时执行多个迭代。我的测试机器是一个Sandy Bridge处理器,它能够支持2x128b负载、1x128b存储和1x256b向量FP乘法(每个周期)。如果加载是微融合UOP的内存源操作数,则可能还有一个或两个向量或整数运算。(仅当使用256b AVX加载/存储时,2个加载+1个存储吞吐量,否则每个周期只有两个总内存操作(最多一个存储))

查看程序集(为了简洁起见,我将省略它),编译器似乎展开了循环,从而减少了循环开销。但它并没有完全实现矢量化


内存带宽约为10 GB/s:

最简单的测试方法是通过
memset()

#包括
#包括
使用std::cout;
使用std::endl;
int main(){
常数int LEN=1 Y
  • 在当前的高端系统上,
    X
    。但是
    X*(#核心)>Y
  • 回到2013年:沙桥@4 GHz+双通道DDR3@1333 MHz

    • 无矢量化(8字节加载/存储):
      X=32 GB/s
      Y=~17 GB/s
    • 矢量化SSE*(16字节加载/存储):
      X=64 GB/s
      Y=~17 GB/s
    现在是2017年:Haswell-E@4 GHz+四通道DDR4@2400 MHz

    • 无矢量化(8字节加载/存储):
      X=32 GB/s
      Y=~70 GB/s
    • 矢量化AVX*(32字节加载/存储):
      X=64 GB/s
      Y=~70 GB/s
    (对于Sandy Bridge和Haswell,缓存中的体系结构限制将带宽限制在16字节/周期左右,而不考虑SIMD宽度。)

    因此,如今,单个线程并不总是能够饱和内存带宽。您需要进行矢量化以达到
    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);