【问题标题】:Collective or individual loops for best performance?集体或个人循环以获得最佳性能?
【发布时间】:2017-03-15 21:21:45
【问题描述】:

假设我有 N 在 C(或任何其他语言 - 我想这并不重要)中相同大小的连续数组。我想遍历这些数组并对每个数组元素执行一些操作。这可以通过单个循环来实现,因为所有 N 数组的大小相同。但是,由于计算机内存的工作方式,为每个数组执行单独的循环会更快吗?

具体而言,假设N 非常大,可能有几十亿,使每个数组的大小达到数千兆字节。此外,数组实际上是 3D 的,这意味着完整的“数组循环”实际上是三个嵌套循环。从三个循环变量计算数组指针的算术复杂性与对数组元素的实际操作的复杂性相当,这就是为什么我害怕添加不必要的循环。

“显而易见”的答案是只编写两个程序,看看哪个程序在我的特定情况下表现最好。不过,我想听听一些更深思熟虑的论点/指南来判断这种情况,因为这超出了我自己的编程直觉。

【问题讨论】:

    标签: c arrays performance loops caching


    【解决方案1】:

    我最初的直觉是每个数组循环会更好,因为数据局部性将有助于缓存。

    将所有数组放在一个循环中会污染缓存(在处理第一个数组时,缓存将(部分)由该数组的某些元素填充,但在下一行代码中,您将需要数据第二个数组和缓存将无法提供帮助。

    两种情况下的理论复杂度保持不变。


    但是,这取决于您拥有的数组数量(我现在不是说连续的)。必须一次又一次地从头开始循环可以超过每个循环一个数组的方法可以提供的缓存加速,如下所示:

    Georgioss-MacBook-Pro:~ gsamaras$ cat bigloop.c
    #include <stdio.h>
    #include <sys/time.h>
    #include <time.h>
    
    #define N 100000
    
    typedef struct timeval wallclock_t;
    void wallclock_mark(wallclock_t *const tptr);
    double wallclock_since(wallclock_t *const tptr);
    
    // gcc -Wall -O3 bigloop.c -o bigloop
    int main(void)
    {
        int a[N], b[N], c[N], d[N], e[N], i;
    
        wallclock_t  t;
        double  s;
    
        wallclock_mark(&t);
        for(i = 0; i < N; ++i)
        {
            a[i] = i * 10 + (i - 2);
            b[i] = i * 9 + (i - 3);
            c[i] = i * 8 + (i - 1);
            d[i] = i * 11 + (i - 5);
            e[i] = i * 5 + (i - 0);
        }
        s = wallclock_since(&t);
        printf("Populating took %.9f seconds wall clock time.\n", s);
    
        wallclock_mark(&t);
        for(i = 0; i < N; ++i)
        {
            a[i] = e[i] + (i - 1);
            b[i] = d[i] + (i + 3);
            c[i] = a[i] - (i + 2);
            d[i] = b[i] + (i - 2);
            e[i] = a[i] + (i - 4);
        }
        s = wallclock_since(&t);
        printf("Load/write took %.9f seconds wall clock time.\n", s);
    
        return 0;
    }
    
    #include <stdio.h>
    #include <sys/time.h>
    #include <time.h>
    
    #define N 100000
    
    typedef struct timeval wallclock_t;
    void wallclock_mark(wallclock_t *const tptr);
    double wallclock_since(wallclock_t *const tptr);
    
    int main(void)
    {
        int a[N], b[N], c[N], d[N], e[N], i;
    
        wallclock_t  t;
        double  s;
    
        wallclock_mark(&t);
        for(i = 0; i < N; ++i)
            a[i] = i * 10 + (i - 2);
        for(i = 0; i < N; ++i)
            b[i] = i * 9 + (i - 3);
        for(i = 0; i < N; ++i)
            c[i] = i * 8 + (i - 1);
        for(i = 0; i < N; ++i)
            d[i] = i * 11 + (i - 5);
        for(i = 0; i < N; ++i)
            e[i] = i * 5 + (i - 0);
        s = wallclock_since(&t);
        printf("Populating took %.9f seconds wall clock time.\n", s);
    
        wallclock_mark(&t);
        for(i = 0; i < N; ++i)
            a[i] = e[i] + (i - 1);
        for(i = 0; i < N; ++i)
            b[i] = d[i] + (i + 3);
        for(i = 0; i < N; ++i)
            c[i] = a[i] - (i + 2);
        for(i = 0; i < N; ++i)
            d[i] = b[i] + (i - 2);
        for(i = 0; i < N; ++i)
            e[i] = a[i] + (i - 4);
        s = wallclock_since(&t);
        printf("Load/write took %.9f seconds wall clock time.\n", s);
    
        return 0;
    }
    

    我省略了时间测量代码,我有 here。结果是:

    Georgioss-MacBook-Pro:~ gsamaras$ ./bigloop 
    Populating took 0.000581000 seconds wall clock time.
    Load/write took 0.000178000 seconds wall clock time.
    Georgioss-MacBook-Pro:~ gsamaras$ ./loop
    Populating took 0.001092000 seconds wall clock time.
    Load/write took 0.000285000 seconds wall clock time.
    

    您可以看到在填充数组时,all-arrays-in-one-loop 获得了一个数量级的加速。此外,处理它们的速度也更快!


    所以,如果我是你,我会同时实施这两种方法并衡量时间! =)

    PS:不要成为过早优化的受害者。如果您有一个想要优化的项目,请分析您的代码以找到瓶颈并专注于此!

    【讨论】:

    • 我看到你没有反汇编代码 - 否则你会看到 -O3 之后发生的事情 -> 所有循环都被删除为死代码。所以你的代码什么也证明不了。
    • 感谢安蒂的评论!
    • @gsamaras “一个数量级的加速”?更像是二分之一。所以你使用单个循环获得了最佳性能,但这与假设的缓存加速相反?
    • @Anty “所有的循环都被当作死代码删除”是什么意思?你的意思是 -O3 将许多循环编译成一个大循环吗?那么,性能差异从何而来?
    • 否 - 这意味着它们已被完全删除,因为 a[N]、b[N]、c[N]、d[N] 和 e[N] 从未用作任何表达式的一部分有副作用。将它们输出到控制台以禁止此效果就足够了。使用 objdump 或 Godbolt 检查程序集。
    猜你喜欢
    • 2013-05-07
    • 1970-01-01
    • 2018-12-19
    • 1970-01-01
    • 1970-01-01
    • 2022-01-20
    • 1970-01-01
    • 1970-01-01
    • 2022-01-23
    相关资源
    最近更新 更多