C语言 如何优化这些循环(禁用编译器优化)?

hiz5n14c  于 2023-04-29  发布在  其他
关注(0)|答案(3)|浏览(445)

我需要在不使用编译器优化标志的情况下优化一些 for 循环以提高速度(用于学校作业)。
给定一个特定的Linux服务器(学校拥有),一个令人满意的改进是使其运行在7秒以下,一个很大的改进是使其运行在5秒以下。我这里的代码得到了大约5。6秒。我想我可能需要用指针来让它走得更快,但我不确定。我有什么选择?
文件必须保留50行或更少(不包括注解)。

#include <stdio.h>
#include <stdlib.h>

// You are only allowed to make changes to this code as specified by the comments in it.

// The code you submit must have these two values.
#define N_TIMES        600000
#define ARRAY_SIZE     10000

int main(void)
{
    double    *array = calloc(ARRAY_SIZE, sizeof(double));
    double    sum = 0;
    int        i;

    // You can add variables between this comment ...
    register double sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0, sum5 = 0, sum6 = 0, sum7 = 0, sum8 = 0, sum9 = 0;
    register int j;
    // ... and this one.

    printf("CS201 - Asgmt 4 - \n");

    for (i = 0; i < N_TIMES; i++)
    {
        // You can change anything between this comment ...
        for (j = 0; j < ARRAY_SIZE; j += 10)
        {
            sum += array[j];
            sum1 += array[j + 1];
            sum2 += array[j + 2];
            sum3 += array[j + 3];
            sum4 += array[j + 4];
            sum5 += array[j + 5];
            sum6 += array[j + 6];
            sum7 += array[j + 7];
            sum8 += array[j + 8];
            sum9 += array[j + 9];
        }
        // ... and this one. But your inner loop must do the same
        // number of additions as this one does.
    }

    // You can add some final code between this comment ...
    sum += sum1 + sum2 + sum3 + sum4 + sum5 + sum6 + sum7 + sum8 + sum9;
    // ... and this one.

    return 0;
}
u0njafvf

u0njafvf1#

重新发布我从optimized sum of an array of doubles in C的答案的修改版本,因为这个问题被投票否决为-5。另一个问题的OP更多地将其表述为“还有什么可能”,所以我接受了他的话,并对当前CPU硬件的矢量化和调优进行了信息转储。:)
这个问题的OP最终说他不允许使用高于-O0的编译器选项,我猜这里也是这样。
总结:

***为什么使用-O0会扭曲事物(不公平地惩罚在正常编译器的正常代码中很好的东西)。**使用-O0(gcc/clang默认值),这样你的循环就不会被优化掉,这不是一个有效的借口,也不是一个有用的方法来找出启用正常优化后什么会更快。(另请参阅 * Idiomatic way of performance evaluation? *,了解有关基准测试方法和陷阱的更多信息,例如启用优化但仍然阻止编译器优化您想要测量的工作的方法。)

  • 作业中的错误。
  • 优化的类型。FP延迟与吞吐量和依赖链。链接到Agner Fog的网站。(优化的基本阅读)。
  • 让编译器优化它的实验(在修复它不优化之后)。自动矢量化的最佳结果(无源代码更改):gcc:最佳向量化循环的一半快。clang:与手动矢量化循环的速度相同。
  • 更多关于为什么更大的表达式只在-O0上更好的评论。
  • 在没有-ffast-math的情况下对源代码进行更改以获得良好的性能,使代码更接近我们希望编译器做的事情。还有一些在现实世界中毫无用处的规则律师的想法。
  • 使用GCC架构中立向量对循环进行向量化,以查看自动向量化编译器与理想asm代码的性能有多接近(因为我检查了编译器输出)。

我认为这个作业的重点是在没有编译器优化的情况下使用C来教授汇编语言的性能优化。这是愚蠢的。它将编译器在真实的生活中为您做的事情与需要源代码级别更改的事情混合在一起。
参见Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?
-O0不只是“不优化”,它使编译器在每个语句之后将变量存储到内存中,而不是将它们保存在寄存器中。它这样做是为了让您在使用gdb设置断点并 * 修改 * C变量的值(在内存中)时获得“预期”结果。或者即使你jump到同一函数中的另一行。因此,每个C语句都必须编译成一个独立的asm块,该块以内存中的所有变量开始和结束。对于像gcc这样已经是transforms through multiple internal representations of program flow on the way from source to asm的现代可移植编译器,-O0的这一部分需要显式地 * 去优化 * 它的数据流图返回到单独的C语句中。这些store/reloads会延长每个循环携带的依赖链,所以如果循环计数器保存在内存中,那么对于小循环来说是可怕的。(例如,对于inc reg,每次迭代1个周期,而对于对于inc [mem],在紧密循环中创建循环计数器更新的瓶颈)。
对于gcc -O0
register关键字
让gcc将var保存在寄存器中而不是内存中,因此可以在紧密循环中产生很大的差异(Example on the Godbolt Compiler explorer)。但这只是-O0。在真实的代码中,register是没有意义的:编译器尝试最佳地使用变量和临时变量的可用寄存器。register已经在ISO C++11(但不是C11)中被弃用,there's a proposal to remove it from the language沿着其他过时的东西,如三字母。
由于涉及到额外的变量,-O0对数组索引的影响比指针递增要大一些。
数组索引通常使代码更容易阅读。编译器有时候无法优化像array[i*width + j*width*height]这样的东西,所以最好改变源代码来进行 * 强度降低 * 优化,将乘法转换为+=加法。
在asm级别,数组索引与指针递增接近相同的性能。(例如,x86具有像[rsi + rdx*4]一样快的寻址模式。except on Sandybridge and later.)编译器的工作就是通过使用指针递增来优化代码,即使源代码使用数组索引时也是如此,因为这样更快。
为了获得良好的性能,你必须知道编译器能做什么,不能做什么。一些优化是“脆弱的”,对源代码的一个看似无害的小更改将阻止编译器进行优化,而优化对于某些代码的快速运行至关重要。(例如,从循环中拉出常数计算,或证明不同分支条件如何彼此相关的东西,并进行简化。)
除此之外,它是一个垃圾示例,因为它没有任何东西来阻止智能编译器优化整个过程。它甚至不打印总数。甚至gcc -O1(而不是-O3)也放弃了一些循环。
(You可以通过在最后打印sum来解决这个问题。gcc和clang似乎没有意识到calloc会返回零内存,并将其优化到0.0。下面是我的代码。)

通常你会把你的代码放在一个函数中,然后在另一个文件中从main()循环调用它。并单独编译它们,而不进行整个程序的跨文件优化,因此编译器无法根据您调用它的编译时常量进行优化。repeat-loop如此紧密地缠绕在数组上的实际循环周围,这对gcc的优化器造成了严重破坏(见下文)。
另外,这个问题的另一个版本有一个未初始化的变量。看起来long int help是由那个问题的OP引入的,而不是教授。因此,我将不得不将我的“彻头彻尾的废话”降级为仅仅“愚蠢”,因为代码甚至没有在最后打印结果。这是让编译器在这样的微基准测试中不优化所有内容的最常见方法。
我想你的教授提到了一些关于性能的事情。这里有很多不同的东西可以发挥作用,其中许多我认为在第二年的CS课程中没有提到。
除了使用openmp的多线程之外,还有使用SIMD的矢量化。还有针对现代流水线CPU的优化:具体地,避免具有一个长依赖性链。
更多基本阅读:

你的编译器手册也是必不可少的,尤其是。用于浮点代码。浮点具有有限的精度,并且是 * 非 * 关联的。最后的和 * 确实 * 取决于你做加法的顺序。通常舍入误差的差异很小,所以如果你使用-ffast-math允许的话,编译器可以通过重新排序来获得很大的加速。
而不是像sum0那样展开keep multiple accumulators which you only add up at the end。. sum9展开10。FP指令具有中等延迟但高吞吐量,因此您需要保持多个FP操作处于运行状态,以保持浮点执行单元饱和。
如果您需要在下一个操作开始之前完成最后一个操作的结果,则会受到延迟的限制。对于FP添加,即每3个循环一次。在Intel Sandybridge、IvB、Haswell和Broadwell中,FP add的吞吐量为每个周期一个。因此,你需要保持至少3个独立的操作,可以在飞行中一次饱和的机器。For Skylake2 per cycle with latency of 4 clocks(对于Skylake,FMA下降到4个周期延迟。)
在这种情况下,还有一些基本的东西,比如把东西从循环中拉出来。g. help += ARRAY_SIZE

编译器选项

让我们先看看编译器能为我们做些什么。
我从原来的内部循环开始,只取出了help += ARRAY_SIZE,并在最后添加了printf,这样gcc就不会优化所有内容。让我们尝试一些编译器选项,看看我们可以用gcc 4实现什么。9.2(在我的i5 2500k Sandybridge上。3.8GHz最大turbo(轻微OC),3.3GHz持续(与此简短基准测试无关):

  • gcc -O0 fast-loop-cs201.c -o fl:16。第43章完全是个笑话变量在每次操作后存储到内存中,并在下一次操作前重新加载。这是一个瓶颈,并且增加了很多延迟。更不用说失去实际的优化。使用-O0的定时/调谐代码没有用。
  • -O1:4。87s
  • -O2:4.89年代
  • -O3:2。453 s(使用SSE一次执行2个操作。当然,我使用的是64位系统,所以对-msse2的硬件支持是基本的。)
  • -O3 -ffast-math -funroll-loops:2.439s
  • -O3 -march=sandybridge -ffast-math -funroll-loops:1.275 s(使用AVX一次做4个。)
  • -Ofast ...:无增益
  • -O3 -ftree-parallelize-loops=4 -march=sandybridge -ffast-math -funroll-loops:0m2。375s真实的,0m8.500 s用户。看起来锁定开销杀死了它。它总共只生成了4个线程,但内部循环太短,不可能成功:它每次都收集和,而不是给每个线程1/4的外循环迭代。
  • -Ofast -fprofile-generate -march=sandybridge -ffast-math,运行它,然后-Ofast -fprofile-use -march=sandybridge -ffast-math1.275sprofile-guided优化是一个好主意当你可以练习所有相关的代码路径,所以编译器可以做出更好的展开/内联决策。
    *clang-3.5 -Ofast -march=native -ffast-math:1.070s.(clang 3.5太旧了,无法支持-march=sandybridge。你应该更喜欢使用一个新的编译器版本,它足够了解你要调优的目标体系结构,特别是。如果使用-march来编写不需要在旧架构上运行的代码。)

gcc -O3以一种搞笑的方式进行矢量化:内部循环通过将一个数组元素广播到xmm(或ymm)寄存器的所有元素,并对其执行addpd,并行执行外部循环的2(或4)次迭代。所以它看到相同的值被重复地相加,但是即使-ffast-math也不会让gcc把它变成乘法。或者改变循环。
clang-3.5的矢量化效果更好:它向量化了内部循环,而不是外部循环,所以它不需要广播。它甚至使用4个向量寄存器作为4个单独的累加器。它知道calloc只返回16字节对齐的内存(在x86-64 System V上),并且当针对Sandybridge(在Haswell之前)进行调优时,它知道32字节加载在未对齐时会有很大的损失。而且拆分它们并不太昂贵,因为32字节的加载无论如何在加载端口中需要2个周期。

vmovupd -0x60(%rbx,%rcx,8),%xmm4
vinsertf128 $0x1,-0x50(%rbx,%rcx,8),%ymm4,%ymm4

这在以后的CPU上更糟,特别是当数据在运行时碰巧对齐时;请参阅 * Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd? * 了解GCC版本,其中-mavx256-split-unaligned-load在默认情况下与-mtune=generic一起启用。
当我告诉它数组是对齐的时候,它实际上 * 慢 * 了。(使用像array = (double*)((ptrdiff_t)array & ~31);这样的愚蠢黑客,它实际上生成了一条屏蔽低5位的指令,因为clang-3。5不支持gcc的__builtin_assume_aligned。)在这种情况下,它使用4x vaddpd mem, %ymm, %ymm的紧密循环。它只运行约0。65 insns每周期(和0.93 uops / cycle),因此瓶颈不是前端。
我用调试器检查了一下,calloc确实返回了一个16的奇数倍的指针。(用于大分配的glibc倾向于分配新页,并将簿记信息放在初始字节中,总是与任何宽于16的边界不对齐。)因此,32 B内存访问中有一半是通过缓存线进行的,这导致了速度的大幅下降。在Sandybridge上,当你的指针是16 B对齐而不是32 B对齐时,做两个单独的16 B加载会稍微快一点。(gcc为-march=sandybridge启用了-mavx256-split-unaligned-load...-store,也为默认的tune=generic启用了-mavx,这是not so good,特别是对于Haswell或通常由编译器对齐的内存,编译器并不知道它。)

源级变更

从clang beating gcc中我们可以看出,多个累加器非常出色。最明显的方法是:

for (j = 0; j < ARRAY_SIZE; j+=4) {  // unroll 4 times
    sum0 += array[j];
    sum1 += array[j+1];
    sum2 += array[j+2];
    sum3 += array[j+3];
}

然后直到外循环结束后才将4个累加器合二为一。
你的(从另一个问题)来源改变

sum += j[0]+j[1]+j[2]+j[3]+j[4]+j[5]+j[6]+j[7]+j[8]+j[9];

实际上也有类似的效果,这要归功于无序的执行。每组10个是一个独立的依赖关系链。操作顺序规则说,j值首先被加在一起,然后被加到sum。因此,循环携带的依赖链仍然只是一个FP添加的延迟,并且对于每个10个组有很多独立的工作。每个组是9个加法的单独依赖链,并且采取足够少的指令以供乱序执行硬件看到下一链的开始,并且找到并行性以保持那些中等延迟、高吞吐量FP执行单元被馈送。
对于-O0,正如你愚蠢的赋值所要求的,值在每个语句的末尾存储到RAM中。编写更长的表达式而不更新任何变量,甚至是临时变量,将使-O0运行得更快,但这不是一个有用的优化。不要把时间浪费在那些只对-O0有帮助的更改上。而不是以牺牲可读性为代价。
使用4个累加器变量,直到外部循环结束才将它们加在一起,这会使clang的自动向量化器失效。它仍然只运行在1。66 s(而gcc的非向量化-O2只有一个累加器,为4.89)。即使没有-ffast-mathgcc -O2也得到1。66 s用于此源变更。注意,ARRAY_SIZE是4的倍数,所以我没有包含任何清理代码来处理最后3个元素(或者避免阅读超过数组末尾,这将发生在现在写入的情况下)。这样做的时候很容易出错,读到数组的末尾。
另一方面,GCC确实将其向量化,但它也将内部循环悲观(非优化)为单个依赖链。我认为它再次进行了外部循环的多次迭代。

使用gcc的平台无关的vector扩展,我写了一个编译成明显最优代码的版本:

// compile with gcc -g -Wall -std=gnu11 -Ofast -fno-tree-vectorize -march=native fast-loop-cs201.vec.c -o fl3-vec

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <assert.h>
#include <string.h>

// You are only allowed to make changes to this code as specified by the comments in it.

// The code you submit must have these two values.
#define N_TIMES     600000
#define ARRAY_SIZE   10000

int main(void)
{
    double  *array = calloc(ARRAY_SIZE, sizeof(double));
    double  sum = 0;
    int     i;

    // You can add variables between this comment ...
    long int help = 0;

    typedef double v4df __attribute__ ((vector_size (8*4)));
    v4df sum0={0}, sum1={0}, sum2={0}, sum3={0};

    const size_t array_bytes = ARRAY_SIZE*sizeof(double);
    double *aligned_array = NULL;

    // this more-than-declaration could go in an if(i == 0) block for strict compliance with the rules
    if ( posix_memalign((void**)&aligned_array, 32, array_bytes) ) {
        exit (1);
    }
    memcpy(aligned_array, array, array_bytes);  // In this one case: faster to align once and have no extra overhead for N_TIMES through the loop

    // ... and this one.

    // Please change 'your name' to your actual name.
    printf("CS201 - Asgmt 4 - I. Forgot\n");

    for (i = 0; i < N_TIMES; i++) {

        // You can change anything between this comment ...
    /*
    #if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 407 // GCC 4.7 or later.
        array = __builtin_assume_aligned(array, 32);
    #else
        // force-align for other compilers.  This loop-invariant will be done outside the loop.
        array = (double*) ((ptrdiff_t)array & ~31);
    #endif
    */

        assert ( ARRAY_SIZE / (4*4) == (ARRAY_SIZE+15) / (4*4) );  // We don't have a cleanup loop to handle where the array size isn't a multiple of 16

        // incrementing pointers can be more efficient than indexing arrays
        // esp. on recent Intel where micro-fusion only works with one-register addressing modes
        // of course, the compiler can always generate pointer-incrementing asm from array-indexing source
        const double *start = aligned_array;

        while ( (ptrdiff_t)start & 31 ) {
            // annoying loops like this are the reason people use aligned buffers
            sum += *start++;        // scalar until we reach 32B alignment
            // in practice, this loop doesn't run, because we copy into an aligned buffer
            // This will also require a cleanup loop, and break our multiple-of-16 doubles assumption.
        }

        const v4df *end = (v4df *)(aligned_array+ARRAY_SIZE);
        for (const v4df *p = (v4df *)start ; p+3 < end; p+=4) {
            sum0 += p[0];      // p+=4 increments the pointer by 4 * 4 * 8 bytes
            sum1 += p[1];       // make sure you keep track of what you're incrementing
            sum2 += p[2];
            sum3 += p[3];
        }

        // the compiler might be smart enough to pull this out of the inner loop
        // in fact, gcc turns this into a 64bit movabs outside of both loops :P
        help+= ARRAY_SIZE;

            // ... and this one. But your inner loop must do the same
            // number of additions as this one does.

        /* You could argue legalese and say that
         if (i == 0) {
             for (j ...)
                 sum += array[j];
             sum *= N_TIMES;
         }
         * still does as many adds in its *INNER LOOP*, but it just doesn't run it as often
         */
    }

    // You can add some final code between this comment ...
    sum0 = (sum0 + sum1) + (sum2 + sum3);
    sum += sum0[0] + sum0[1] + sum0[2] + sum0[3];
    printf("sum = %g; help=%ld\n", sum, help);  // defeat the compiler.

    free (aligned_array);
    free (array);  // not strictly necessary, because this is the end of main().  Leaving it out for this special case is a bad example for a CS class, though.
    // ... and this one.

    return 0;
}

内部循环编译为:

4007c0:  c5 e5 58 19     vaddpd (%rcx),%ymm3,%ymm3
4007c4:  48 83 e9 80     sub    $0xffffffffffffff80,%rcx # subtract -128, because 
                                        # -128 fits in imm8 instead of requiring 
                                        # an imm32 to encode add $128, %rcx
4007c8:  c5 f5 58 49 a0  vaddpd -0x60(%rcx),%ymm1,%ymm1  # one-register addressing 
                                                         # mode can micro-fuse
4007cd:  c5 ed 58 51 c0  vaddpd -0x40(%rcx),%ymm2,%ymm2
4007d2:  c5 fd 58 41 e0  vaddpd -0x20(%rcx),%ymm0,%ymm0
4007d7:  4c 39 c1        cmp    %r8,%rcx                 # compare with end with p
4007da:  75 e4           jne    4007c0 <main+0xb0>

(For更多信息,请参阅godbolt编译器资源管理器中的联机编译器输出。-xc编译器选项编译为C,而不是C++。内部循环是从.L3jne .L3。参见x86标签wiki获取x86 asm链接。另见this q&a about micro-fusion not happening on SnB-family,Agner Fog的指南没有涵盖)。
关于Sandybridge:

perf stat -e task-clock,cycles,instructions,r1b1,r10e,stalled-cycles-frontend,stalled-cycles-backend,L1-dcache-load-misses,cache-misses ./fl3-vec

输出:

CS201 - Asgmt 4 - I. Forgot
sum = 0; help=6000000000

Performance counter stats for './fl3-vec':

  1086.571078  task-clock (msec)       #    1.000 CPUs utilized
4,072,679,849  cycles                  #    3.748 GHz
2,629,419,883  instructions            #    0.65  insns per cycle
                                       #    1.27  stalled cycles per insn
4,028,715,968  r1b1                    # 3707.733 M/sec  # unfused uops
2,257,875,023  r10e                    # 2077.982 M/sec  # fused uops. Lower than insns because of macro-fusion
3,328,275,626  stalled-cycles-frontend #   81.72% frontend cycles idle
1,648,011,059  stalled-cycles-backend  #   40.47% backend  cycles idle
  751,736,741  L1-dcache-load-misses   #  691.843 M/sec
       18,772  cache-misses            #    0.017 M/sec

  1.086925466 seconds time elapsed

(With更现代的perf,我会分别使用uops_issued.any(融合域)和uops_executed.thread(未融合域)来代替r10 e和r1 b1。使用perf list查看CPU上的可用事件及其说明。)
每周期低指令数是L2高速缓存带宽的瓶颈。内部循环使用4个单独的累加器,我用gdb检查了指针是否对齐。所以缓存-银行冲突不是问题。Sandybridge L2高速缓存可以在一个周期中传送32 B,这可以跟上每个周期一个32 B FP向量添加。但是L2带宽无法在Intel SnB / Haswell / Skylake CPU上维持每时钟1次峰值传输。没有足够的行填充缓冲区来保持足够的未命中以维持每个周期的峰值吞吐量,或者其他一些限制。
从L1加载32 B需要2个周期(直到Haswell,英特尔才使32 B加载成为单周期操作)。但是,有2个加载端口,因此持续吞吐量为每个周期32 B(我们没有达到)。
性能计数器指示一个相当高的L1缓存命中率,因此从L2到L1的硬件预取似乎正在完成其工作。

0.65每周期的指令数仅为使向量FP加法器饱和的大约一半。IACA表示,如果加载全部命中L1d缓存,则循环将在每次迭代中运行4个周期。即使加载端口和端口1(FP加法器存在的地方)饱和。
另请参阅Single Threaded Memory Bandwidth on Sandy Bridge(英特尔论坛线程,其中讨论了限制吞吐量的因素以及latency * max_concurrency如何成为一个可能的瓶颈。另请参阅Enhanced REP MOVSB for memcpy问题答案的“延迟绑定平台”部分有限的内存并发性是加载和存储的瓶颈,但对于加载prefetch into L2 does mean you might not be limited purely by Line Fill buffers for outstanding L1D misses来说则是如此。
将ARRAY_SIZE减少到1008(16的倍数),并将N_TIMES增加10倍,使运行时间降低到0。5块1块每周期68insns。(内部循环是用于4个FP加法的总共7个指令,因此我们最终使向量FP加法单元和加载端口饱和。)循环平铺是一个好得多的解决方案,见下文。
Intel CPU只有32k的L1数据和L1指令缓存。我认为你的阵列只能勉强适应64kiB L1D on an AMD K10 (Istanbul) CPU,而不是Bulldozer-family (16kiB L1D)或Ryzen(32kiB L1D)。
Gcc试图通过将相同的值广播到并行加法中来实现向量化,这看起来并不疯狂。如果它设法做到了这一点(使用多个累加器来隐藏延迟),这将允许它仅用一半的存储器带宽使向量FP加法器饱和。事实上,这几乎是一个清洗,可能是因为在广播的开销。
而且,这很傻。N_TIMES只是一个重复的工作。我们实际上并不想优化多次执行相同的工作。除非我们想赢这种愚蠢的任务。一个源代码级的方法是在允许修改的代码部分增加i

for (...) {
    sum += a[j] + a[j] + a[j] + a[j];
}
i += 3;  // The inner loop does 4 total iterations of the outer loop

更实际地说,为了处理这个问题,你可以交换你的循环(在数组上循环一次,将每个值相加N_TIMES次)。我想我已经读到英特尔的编译器有时会为你做这件事。

一种更通用的技术称为缓存阻塞或循环平铺。这个想法是在适合缓存的小块中处理输入数据。根据您的算法,可以在一个块上执行不同的阶段,然后重复下一个块,而不是让每个阶段在整个输入上循环。和往常一样,一旦你知道一个技巧的正确名称(而且它确实存在),你就可以在谷歌上搜索到大量的信息。

您可以按照自己的方式在允许修改的代码部分的if (i == 0)块中放置一个可互换的循环。它仍将执行相同数量的加法,但以更适合缓存的顺序执行。

xriantvc

xriantvc2#

你 * 可能 * 在正确的轨道上,尽管你需要测量它才能确定(我通常的建议是 * 测量,而不是猜测 * 在这里似乎有点多余,因为作业的整个 * 点 * 就是测量)。
优化编译器可能不会看到太大的差异,因为它们在这方面非常聪明,但是,由于我们不知道它将在什么优化级别编译,您可能会得到实质性的改进。
要在内部循环中使用指针,首先要添加一个指针变量:

register double *pj;

然后将循环改为:

for (pj = &(array[0]); pj < &(array[ARRAY_SIZE]); j++) {
        sum += *j++;
        sum1 += *j++;
        sum2 += *j++;
        sum3 += *j++;
        sum4 += *j++;
        sum5 += *j++;
        sum6 += *j++;
        sum7 += *j++;
        sum8 += *j++;
        sum9 += *j;
    }

这使得循环中的加法运算量保持不变(当然,假设您将+=++算作加法运算符),但基本上使用指针而不是数组索引。
由于我的系统上没有优化1,这使它从9下降。868秒(CPU时间)到4.84秒。您的里程可能会有所不同。
1* 对于 * 优化水平-O3,* 两者 * 均报告为取0。001秒,正如前面提到的,优化者非常聪明。然而,考虑到你看到的是5秒以上,我建议它不是在优化的情况下编译的。
顺便说一句,这是一个很好的理由,为什么通常建议以可读的方式编写代码,让编译器负责让它运行得更快。虽然我在优化方面的尝试几乎使速度翻了一番,但使用-O3使它的运行速度快了大约一万倍:-)

vh0rcniy

vh0rcniy3#

在做任何其他事情之前,请尝试更改编译器设置以生成更快的代码。有一般的优化,编译器可能会自动向量化。
你要做的就是尝试几种方法,看看哪种方法最快。作为一个目标,尝试达到一个周期,每次添加或更好。
每个循环的迭代次数:你同时加10个数字。这可能是因为您的处理器没有足够的寄存器,或者它有更多。我会测量4 5 6 7 8 9 10 11 12 13 14的时间.. sums per loop.
总数:有一个以上的总和意味着延迟不会影响您,只是吞吐量。但超过四个或六个可能没有帮助。尝试四次求和,每次循环迭代4、8、12、16次。或者六个求和,具有6、12、18次迭代。
缓存:您正在运行一个80,000字节的数组。可能比L1缓存更多。将阵列拆分为2或4个部分。在两个或四个子数组上执行一个外部循环,下一个循环从0到N_TIMES - 1,内部循环将值相加。
然后,您可以尝试使用向量操作,或多线程代码,或使用GPU来完成工作。
如果你被迫使用无优化,那么“注册”关键字可能实际上工作。

相关问题