【问题标题】:Can reducing loop times in C++ codes help increase the speed?减少 C++ 代码中的循环时间有助于提高速度吗?
【发布时间】:2016-03-10 23:33:17
【问题描述】:

我举以下例子来说明我的问题:

void fun(int i, float *pt)
{
    // do something based on i
    std::cout<<*(pt+i)<<std::endl;
}
const unsigned int LOOP = 2000000007;

void fun_without_optmization()
{
    float *example;
    example = new float [LOOP];
    for(unsigned int i=0; i<LOOP; i++)
    {
        fun(i,example);            
    }
    delete []example;
}
void fun_with_optimization()
{
    float *example;
    example = new float [LOOP];
    unsigned int unit_loop = LOOP/10;
    unsigned int left_loop = LOOP%10;
    pt = example;
    for(unsigend int i=0; i<unit_loop; i++)
    {
        fun(0,pt); 
        fun(1,pt);  
        fun(2,pt);
        fun(3,pt);  
        fun(4,pt);
        fun(5,pt);
        fun(6,pt);  
        fun(7,pt);
        fun(8,pt);  
        fun(9,pt);
        pt=pt+10;
    }
    delete []example;
}

据我了解,函数 fun_without_optimization() 和函数 fun_with_optimization() 应该执行相同的操作。第二个函数比第一个更好的唯一理由是fun 中的指针计算变得简单。为什么第二个功能更好的任何其他论点?

【问题讨论】:

  • 第二个功能是手动展开循环的不明智尝试。编译器会更有效地为您展开这个循环。
  • 这种“优化”是基本的,任何编译器都可以比人类更好地优化
  • 与大多数此类问题一样,最好的方法是分析代码并查看哪个更快。
  • fun 中的指针计算变得简单”是什么意思? fun 在这两种情况下不是完全相同吗?
  • 第二个函数的bug比较多,一定更好。 (真正的程序员使用Duff's device,当然。)

标签: c++ performance


【解决方案1】:

展开执行 I/O 的循环就像在肯尼迪机场将 B747 的着陆跑道从伦敦向东移动一英寸。

【讨论】:

  • 彼得,我强烈建议不要将地带再往东移动。可能最终在牙买加湾。
【解决方案2】:

Re:“还有什么其他论据为什么第二个功能更好?” - 你会接受解释为什么它更好的答案吗?

  1. 手动展开循环容易出错,您的代码清楚地说明了这一点:您忘记处理尾部 left_loop
  2. 至少几十年来,编译器都会为您进行这种优化。
  3. 您如何知道在展开的循环中放置的最佳迭代次数?您是否针对特定的缓存大小并以字节为单位计算汇编指令的长度?编译器可能会。
  4. 弄乱原本干净的循环可能会妨碍其他优化,例如使用 SIMD。

底线是:如果您知道编译器不知道的某些内容(运行时数据的特定模式、目标执行环境的详细信息等),并且您知道自己在做什么 - 您可以尝试手动循环展开。但即便如此 - 个人资料。

【讨论】:

  • 编译器在展开时往往不擅长使用多个累加器来创建多个依赖链来隐藏延迟。这在 FP 循环中往往很重要(例如,Haswell 上的最大 FMA 吞吐量需要保持 10 个 FMA 处于运行状态,因为它们有 5 个周期延迟和一个每 0.5c 吞吐量)。对于带有循环携带依赖链的 FP 循环(例如对数组求和),它可以帮助使用多个向量累加器手动展开。
  • @PeterCordes - 太棒了!但这只是说明了我的两个观点:(1)if you know something that your compiler doesn't 和(2)you know what you are doing :) 但即便如此 - 你对 Haswell 有不同的构建吗?如果您的代码将在另一个处理器上运行怎么办?
  • 在不同的 x86 处理器上,您仍然会受到 FMA 吞吐量的限制。一些未来的处理器可能需要比 Haswell 更多的 FMA 在运行中,但是Skylake actually reduces FMA latency to 4c,因此它只需要 8 个 FP add/mul/FMA 在运行中就可以使其 FMA 单元饱和。一个聪明的编译器总是可以重组事物以使用更多的累加器。一个非常聪明的编译器甚至可以重组东西以使用更少的累加器(例如,当编译只有 8 个向量寄存器的 32 位时),但你可能只会得到带有溢出的蹩脚代码。
  • 由于您必须对自己进行矢量化(例如使用 AVX 内在函数)以使用多个 vector 累加器,因此无论如何您都需要在 ifdef 中使用该版本的循环或函数。因此,对于完全不同的架构,您将回退到纯 C 标量代码(希望为 ARM NEON 或 PPC altivec 或 w/e 自动矢量化)。
【解决方案3】:

你描述的技术叫做loop unrolling;这可能会提高性能,因为评估控制结构(更新循环变量和检查终止条件)的时间变得更小。但是,体面的编译器可以为您做到这一点,如果手动完成,代码的可维护性会降低。

【讨论】:

    【解决方案4】:

    这是一种用于并行架构(支持 VLIW 指令的架构)的优化技术。根据架构支持的 DALU(最常见的 4 个)和 ALU(最常见的 2 个)单元的数量,以及代码支持的“并行化”级别,一个周期内可以执行多条指令。

    所以这段代码:

    for (int i=0; i<n;i++) //n multiple of 4, for simplicity
       a+=temp; //just a random instruction
    

    如果重写如下,实际上会在并行架构上执行得更快:

    for (int i=0;i<n ;i+=4)
    {
        temp0 = temp0 +temp1; //reads and additions can be executed in parallel
        temp1 = temp2 +temp3;
        a=temp0+temp1+a;
    }
    

    您可以并行化代码的数量是有限制的,这是由 CPU 所拥有的物理 ALU/DALU 施加的限制。这就是为什么在尝试(正确)优化代码之前了解您的架构很重要的原因。

    不止于此:您要优化的代码必须是连续的代码块,这意味着没有跳转(没有函数调用,没有流指令的机会),以实现最高效率。

    编写您的代码,例如:

        for(unsigend int i=0; i<unit_loop; i++)
        {
            fun(0,pt); 
            fun(1,pt);  
            fun(2,pt);
            fun(3,pt);  
            fun(4,pt);
            fun(5,pt);
            fun(6,pt);  
            fun(7,pt);
            fun(8,pt);  
            fun(9,pt);
            pt=pt+10;
        } 
    

    不会做太多,除非编译器内联函数调用;无论如何,它看起来像很多指令......

    另外一点:虽然在优化代码时您总是必须使用编译器,但您永远不应该只依赖它来最大限度地优化您的代码。请记住,当您可能对特定情况感兴趣时,编译器会处理“一般情况”——这就是为什么有些编译器有特殊指令来帮助优化过程。

    【讨论】:

    • 我从未听说过循环展开专门针对 VLIW CPU 的说法。它仍然减少了普通 CPU 上的循环开销。 VLIW ISA 的编译器需要善于自行寻找并行性,而循环展开确实是一种很好的解决方法。
    • @PeterCordes 现在大多数架构都是并行的,但如果你也这样做——循环展开——你可能会看到性能下降(因为堆栈溢出)。循环展开等技术首先被引入用于 DSP 代码优化 - 因此 VLIW 参考。
    • 这取决于你如何展开。一些编译器(如 clang)喜欢通过 just repeating most of the loop body, like in this case 展开,而不交错迭代。所以它依赖于 CPU 的 OOO 窗口来提取并行度。如果有的话,这不会增加太多的套准压力。此外,en.wikipedia.org/wiki/Loop_unrolling 表示该技术至少从 1977 年就已为人所知(请参阅参考资料中的日期)。这正好是最早的 DSP 的日期。他们不是 1977 年的 VLIW,是吗?
    • 另外,假设您的编译器可以内联简单的小函数是安全的(除非您将定义隐藏在不同的编译单元中)。现代编译器在这方面做得很好,至少 gcc/clang 是。用于不常见架构的专有供应商提供的编译器可能会很糟糕。
    猜你喜欢
    • 2021-07-30
    • 1970-01-01
    • 2018-08-06
    • 1970-01-01
    • 1970-01-01
    • 2017-04-21
    • 2015-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多