【问题标题】:What is the overhead in splitting a for-loop into multiple for-loops, if the total work inside is the same? [duplicate]如果内部的总工作量相同,将 for 循环拆分为多个 for 循环的开销是多少? [复制]
【发布时间】:2012-12-01 18:18:34
【问题描述】:

像这样拆分for-loop 的开销是多少,

int i;

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

像这样进入多个for-loops?

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 -loop 将花费相当于两次 int 分配的费用。”但是,如果不是那么简单,我理解(并且不会感到惊讶)。

非常感谢,提前。

【问题讨论】:

标签: c++ performance for-loop


【解决方案1】:

通常有太多的因素在起作用......而且很容易证明两种方式:

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

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

这几乎是显而易见的,因为您需要循环 3 次而不是一次,并且您需要 3 次遍历整个数组而不是 1 次。

另一方面,如果你看看这个问题:Why are elementwise additions much faster in separate loops than in a combined loop?

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

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


从中可以得到什么?

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

因此,确定此类优化是否会提高性能通常需要反复试验。有了足够的经验,您就可以做出相当自信(受过教育)的猜测。但总的来说,什么都期待。

“每个额外的 for 循环将花费相当于两个 int 分配的成本。”

你说得对,事情没那么简单。事实上,它是如此复杂,以至于这些数字并没有多大意义。由于Out-of-order Execution 和数据依赖性等多种因素,循环迭代可能在一种情况下需要 X 个周期,但在另一种情况下需要 Y 个周期。

性能不仅取决于上下文,而且还因不同的处理器而异。


这是测试代码:

#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 秒

【讨论】:

  • 这个答案是错误的。我想发布我的答案,但问题似乎已被锁定。我在 Godbolt 中尝试了这两个版本,正如我所料,生成的汇编代码(优化 -O3)完全相同。
  • @HAL9000 请注意,这个问题已有 7 年历史。从那时起,编译器肯定得到了改进。
【解决方案2】:

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

基于指令流水线的重新加载,第一种方法会更快,但并不显着。您将通过拆分添加至少 2 个新的分支指令。

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

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

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

【讨论】:

  • 1.减少跳转指令的数量。展开,尽可能少地循环。 2.缓存对齐你的数据,如果不对齐CPU指令需要更多的周期。 3. 如果可以的话,使用预取指令将数据带入一级缓存,这样缓存未命中的情况就会减少。
【解决方案3】:

这将让您清楚地了解一个版本是否比另一个版本快。

#include <array>
#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
            }
        });
}

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • -1 没这么简单。在许多情况下,循环开销很小。在其他情况下,它很大。
    • 我同意你的看法。这取决于单个循环的运行次数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-21
    • 1970-01-01
    • 1970-01-01
    • 2021-01-08
    相关资源
    最近更新 更多