循环效率:合并循环

时间:2018-06-25 10:25:41

标签: c++ performance loops benchmarking

我一直有个想法,减少迭代次数是使程序更高效的 方法。由于我从未真正确认过这一点,因此开始进行测试。

我制作了以下C ++程序,用于测量两个不同函数的时间:

  • 第一个函数执行一个大循环并使用一组变量。
  • 第二个函数执行多个同样大的循环,但每个变量执行一个循环。

完整的测试代码:

    #include <iostream>
    #include <chrono>

    using namespace std;

    int* list1; int* list2;
    int* list3; int* list4;
    int* list5; int* list6;
    int* list7; int* list8;
    int* list9; int* list10;

    const int n = 1e7;

    // **************************************
    void myFunc1()
    {
        for (int i = 0; i < n; i++)
        {
            list1[i] = 2;
            list2[i] = 4;
            list3[i] = 8;
            list4[i] = 16;
            list5[i] = 32;
            list6[i] = 64;
            list7[i] = 128;
            list8[i] = 256;
            list9[i] = 512;
            list10[i] = 1024;
        }

        return;
    }

    // **************************************
    void myFunc2()
    {

        for (int i = 0; i < n; i++)
        {
            list1[i] = 2;
        }
        for (int i = 0; i < n; i++)
        {
            list2[i] = 4;
        }
        for (int i = 0; i < n; i++)
        {
            list3[i] = 8;
        }
        for (int i = 0; i < n; i++)
        {
            list4[i] = 16;
        }
        for (int i = 0; i < n; i++)
        {
            list5[i] = 32;
        }
        for (int i = 0; i < n; i++)
        {
            list6[i] = 64;
        }
        for (int i = 0; i < n; i++)
        {
            list7[i] = 128;
        }
        for (int i = 0; i < n; i++)
        {
            list8[i] = 256;
        }

        for (int i = 0; i < n; i++)
        {
            list9[i] = 512;
        }
        for (int i = 0; i < n; i++)
        {
            list10[i] = 1024;
        }

        return;
    }


    // **************************************
    int main()
    {
        list1 = new int[n]; list2 = new int[n];
        list3 = new int[n]; list4 = new int[n];
        list5 = new int[n]; list6 = new int[n];
        list7 = new int[n]; list8 = new int[n];
        list9 = new int[n]; list10 = new int[n];

        auto start = chrono::high_resolution_clock::now();

        myFunc1();

        auto elapsed = chrono::high_resolution_clock::now() - start;

        long long microseconds = chrono::duration_cast<chrono::microseconds>(elapsed).count();

        cout << "Time taken by func1 (micro s):" << microseconds << endl << endl;

        //

        start = chrono::high_resolution_clock::now();

        myFunc2();

        elapsed = chrono::high_resolution_clock::now() - start;

        microseconds = chrono::duration_cast<chrono::microseconds>(elapsed).count();

        cout << "Time taken by func2 (micro s):" << microseconds << endl << endl;

        delete[] list1; delete[] list2; delete[] list3; delete[] list4;
        delete[] list5; delete[] list6; delete[] list7; delete[] list8;
        delete[] list9; delete[] list10;

        return 0;
    }

现在,我有矛盾的假设:一方面,两个函数的操作量相同,只是设置了一些变量。尽管另一方面,第二个函数要经过10次以上的循环,因此(也许)也应该多花10倍的时间。

结果令人惊讶。在我的PC上,func1()花费约280毫秒,func2()花费约180毫秒,第一个功能实际上较慢而不是较快。

现在要提出问题:我的考试正确吗?合并for循环以最大程度地减少迭代总数是否有用?人们有不同的经历吗?

编辑:我在禁用所有优化的情况下进行了编译,以进行测试。 编辑:更改函数调用的顺序将得到相同的结果。而且,测量的时间变化很小,因此我不必理会平均值。

编辑:我通过-O3优化再次尝试了所有这些操作。尽管确切的度量当然有所不同,但结果的确保持不变。

6 个答案:

答案 0 :(得分:5)

如果我不得不冒险猜测的话,我会说您所看到的是第一个函数中频繁发生内存高速缓存未命中的结果。

myFunc1()本质上是以随机访问的方式执行10e8存储器写入。

myFunc2()正在执行10次连续的10e7个字的内存写入。

在现代内存体系结构上,我希望第二种效率更高。

答案 1 :(得分:3)

从单个循环中获得的好处是,您失去了对循环变量的惩罚。因此,在这种情况下,循环的内容微不足道,这种分配(和测试)的作用很大。

您的示例也没有考虑到;连续内存访问通常比随机访问快。

在一个函数中,循环花费更长的时间(尝试使自己进入睡眠状态而不是赋值状态),您会发现差异的作用很大。

获得性能改进的方法是从数学开始-正确的算法将永远带来最大的改进。最好在手指敲击键盘之前完成此操作。

答案 2 :(得分:3)

尝试对代码进行基准测试时,您需要:

  1. 启用优化标记进行编译。
  2. 多次运行每个测试,以收集平均值

您没有都做。例如,您可以使用-O3,就平均而言,我做到了(我使函数从列表中返回一个元素):

for(int i = 0; i < 100; ++i)        
    dummy = myFunc1();

然后,我得到这样的输出:

Time taken by func1 (micro s):206693

Time taken by func2 (micro s):37898

这证实了您所看到的内容,但是区别是一个数量级(这是非常重要的)。


在单个for循环中,您需要做一次客房整理,并且循环的计数器会增加一次。在几个for循环中,它被扩展了(您需要做的次数与for循环一样多)。当循环的主体有些微不足道时(如您的情况),则可以有所作为。


另一个问题是 数据位置 。第二个函数具有循环,该循环将一次填充一个列表(这意味着将以连续方式访问内存)。在第一个函数的大循环中,您将一次填充列表中的一个元素,最终归结为随机访问内存(因为例如list1将被带入缓存,因为您填充了元素它,然后在代码的下一行中,您将请求list2,这意味着list1现在已经无用了,但是,在第二个函数中,一旦将list1放入缓存中,您将继续从缓存中使用它(而不是必须从内存中获取它),从而大大提高了速度。


我相信这个事实在这里比其他(大循环VS几个小循环)主导。因此,您实际上不是在基准测试所需的基准,而是随机内存访问与连续内存访问

答案 3 :(得分:3)

您的假设基本上是有缺陷的:

  1. 循环迭代不会产生可观的成本。

    这是CPU的优化目标:紧密循环。 CPU优化可以达到将专用电路用于循环计数器(例如PPC bdnz指令)的程度,从而使循环计数器的开销恰好为零。 X86确实需要一个CPU周期或两个afaik,仅此而已。

  2. 影响您性能的因素通常是内存访问

    从L1缓存中获取值已经花费了三到四个CPU周期的延迟。 L1缓存中的单个负载比循环控制具有更多的延迟!更多用于高级缓存。 RAM访问需要永远的时间。

因此,要获得良好的性能,通常需要减少花在访问内存上的时间。可以通过

  • 避免内存访问。

    最有效,最容易被忘记的优化。您不为自己不做的事情付费。

  • 并行化内存访问。

    避免加载某些值,并根据此值获取下一个所需值的地址。这种优化非常棘手,因为它需要清楚地了解不同内存访问之间的依赖关系。

    此优化 可能需要一些循环融合或循环展开才能利用不同循环体/迭代之间的独立性。在您的情况下,循环迭代彼此独立,因此它们已经尽可能并行。

    此外,正如MSalters在注释中正确指出的那样:CPU的寄存器数量有限。有多少取决于体系结构,例如32位X86 CPU只有八个。因此,它根本无法同时处理十个不同的指针。它将需要在堆栈中存储一些指针,从而引入更多的内存访问。显然,这违反了上面关于避免内存访问的观点。

  • 按顺序对内存访问。

    CPU的构建是基于绝大多数内存访问是顺序的,并且为此进行了优化。当您开始访问数组时,CPU通常会很快注意到,并开始预取后续值。

最后一点是您的第一个功能失败:您正在10个完全不同的内存位置访问10个不同的数组之间来回跳转。这会降低CPU推断应从主内存中预取哪些缓存行的能力,从而降低整体性能。

答案 4 :(得分:2)

此代码创建变量:

    list1 = new int[n]; list2 = new int[n];
    list3 = new int[n]; list4 = new int[n];
    list5 = new int[n]; list6 = new int[n];
    list7 = new int[n]; list8 = new int[n];
    list9 = new int[n]; list10 = new int[n];

,但是几乎可以肯定,它不会创建实际的物理页面映射,直到内存被实际修改为止。有关示例,请参见Does malloc lazily create the backing pages for an allocation on Linux (and other platforms)?

因此,您的func1()必须等待创建RAM的实际物理页面,而您的func2()则不必等待。更改顺序,映射时间将归因于func2()的性能。

给定已发布代码的最简单解决方案是在定时运行之前,先运行func1()func2()

如果您不确定在之前已映射了实际的物理内存,则在进行基准测试时,该映射将是您首次修改内存时测量的一部分时间。

答案 5 :(得分:0)

我认为这要复杂得多。一个循环是否比多个循环快取决于几个因素。

程序在一组数据上进行迭代的事实使您付出了一定的代价(增加迭代器或索引;将迭代器/索引与使循环知道已完成的某个值进行比较),因此,如果将循环划分为几个较小的循环,您需要为多次遍历同一组数据支付更高的费用。

另一方面,如果循环较小,则优化器的工作更轻松,并且有更多的方法来优化代码。 CPU还可以使循环运行得更快,通常在小循环时效果最好。

我有一段代码,在将一个循环分成多个较小的循环后,它们变得更快。我还写了一些算法,结果证明当我将几个循环合并为一个循环时,性能会更好。

通常有很多因素,很难预测哪个因素占主导地位,因此答案是,您应该始终测量并检查一些版本的代码以找出哪个速度更快。