关于c ++:如果内部的总工作量相同,那么将for循环拆分成多个for循环的开销是多少?

What is the overhead in splitting a for-loop into multiple for-loops, if the total work inside is the same?

本问题已经有最佳答案,请猛点这里访问。

像这样拆分for循环的开销是多少?

1
2
3
4
5
6
7
8
int i;

for (i = 0; i < exchanges; i++)
{
    // some code
    // some more code
    // even more code
}

变成这样的多个for循环?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i;

for (i = 0; i < exchanges; i++)
{
    // some code
}

for (i = 0; i < exchanges; i++)
{
    // some more code
}

for (i = 0; i < exchanges; i++)
{
    // even more code
}

代码是性能敏感的,但执行后一种操作将显著提高可读性。(在重要的情况下,除了每个循环中的几个访问器外,没有其他循环、变量声明或函数调用。)

我不是一个低级的编程大师,所以如果有人能够比较基本操作来衡量性能的影响,那就更好了,例如,"每个额外的for循环将花费相当于两个int分配的成本。"但是,如果不是那么简单的话,我理解(并且不会感到惊讶)。

非常感谢,提前。


经常有太多的因素在起作用…这两种方法都很容易演示:

例如,拆分以下循环会导致几乎2倍的速度减慢(底部是完整的测试代码):

1
2
3
4
5
for (int c = 0; c < size; c++){
    data[c] *= 10;
    data[c] += 7;
    data[c] &= 15;
}

这几乎说明了显而易见的一点,因为您需要循环3次而不是一次,并且在整个数组中循环3次而不是1次。

另一方面,如果你看一看这个问题:为什么在单独的循环中元素添加比在组合循环中更快?

1
2
3
4
for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

由于内存对齐,有时情况正好相反。

从中得到什么?

几乎任何事情都可能发生。这两种方法都不是很快,而且很大程度上取决于循环中的内容。

因此,确定这种优化是否会提高性能通常是一种尝试和错误。有了足够的经验,你可以做出相当有信心(有教养)的猜测。但总的来说,什么都不要期待。

"Each additional for-loop would cost the equivalent of two int allocations."

你说得对,事情没有那么简单。事实上,它是如此复杂,以至于数字意义不大。循环迭代在一个上下文中可能需要x个循环,但在另一个上下文中可能需要y个循环,这是由于许多因素造成的,例如无序执行和数据依赖性。

它不仅依赖于性能上下文,而且还随处理器的不同而变化。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <time.h>
#include <iostream>
using namespace std;

int main(){

    int size = 10000;
    int *data = new int[size];


    clock_t start = clock();

    for (int i = 0; i < 1000000; i++){
#ifdef TOGETHER
        for (int c = 0; c < size; c++){
            data[c] *= 10;
            data[c] += 7;
            data[c] &= 15;
        }
#else
        for (int c = 0; c < size; c++){
            data[c] *= 10;
        }
        for (int c = 0; c < size; c++){
            data[c] += 7;
        }
        for (int c = 0; c < size; c++){
            data[c] &= 15;
        }
#endif
    }

    clock_t end = clock();
    cout << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
}

输出(一个回路):4.08秒输出(3圈):7.17秒


处理器喜欢使用更高比率的数据指令来跳转指令。分支指令可能会强制处理器清除指令管道并重新加载。

基于指令管道的重新加载,第一种方法会更快,但不会显著。您可以通过拆分添加至少2个新的分支指令。

更快的优化是展开循环。展开循环试图提高数据指令与分支指令的比率,方法是在分支到循环顶部之前在循环内部执行更多指令。

另一个重要的性能优化是组织数据,使其适合处理器的一条缓存线。例如,您可以拆分内部循环来处理单个数据缓存,外部循环会将新项目加载到缓存中。

只有在程序正确、可靠地运行并且环境要求更高的性能之后,才能应用此优化。定义为观察者(动画/电影)、用户(等待响应)或硬件(在关键时间事件之前执行操作)的环境。任何其他目的都是浪费您的时间,因为操作系统(运行并发程序)和存储访问将对您的程序的性能问题做出更多贡献。


这将很好地指示一个版本是否比另一个版本快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include
#include <chrono>
#include <iostream>
#include <numeric>
#include <string>

const int iterations = 100;

namespace
{
    const int exchanges = 200;

    template<typename TTest>
    void Test(const std::string &name, TTest &&test)
    {
        typedef std::chrono::high_resolution_clock Clock;
        typedef std::chrono::duration<float, std::milli> ms;

        std::array<float, iterations> timings;

        for (auto i = 0; i != iterations; ++i)
        {
            auto t0 = Clock::now();

            test();

            timings[i] = ms(Clock::now() - t0).count();
        }

        auto avg = std::accumulate(timings.begin(), timings.end(), 0) / iterations;
        std::cout <<"Average time," << name <<":" << avg << std::endl;
    }
}

int main()
{
    Test("single loop",
        []()
        {
            for (auto i = 0; i < exchanges; ++i)
            {
                // some code
                // some more code
                // even more code
            }
        });

    Test("separated loops",
        []()
        {
            for (auto i = 0; i < exchanges; ++i)
            {
                // some code
            }

            for (auto i = 0; i < exchanges; ++i)
            {
                // some more code
            }

            for (auto i = 0; i < exchanges; ++i)
            {
                // even more code
            }
        });
}

事情很简单。第一个代码类似于在赛道上跑一圈,另一个代码类似于进行一次完整的3圈比赛。所以,三圈比一圈需要更多的时间。但是,如果循环正在做一些需要按顺序完成的事情,并且它们相互依赖,那么第二个代码将完成这些事情。例如,如果第一个循环正在进行一些计算,而第二个循环正在对这些计算进行一些工作,那么两个循环都需要按顺序进行,否则就不需要……