【问题标题】:How much can this c/c++ loop be optimized? [closed]这个 c/c++ 循环可以优化多少? [关闭]
【发布时间】:2010-03-03 15:43:33
【问题描述】:

我是优化方面的新手。我一直在阅读有关如何优化 c++ 代码的一些参考资料,但我很难将其应用于实际代码。因此,我只想收集一些真实世界的优化技术,以了解如何从下面的循环中尽可能多地从 CPU/内存中榨取汁液

double sum = 0, *array;
array = (double*) malloc(T * sizeof(double));
for(int t = 0; t < T; ++t){
sum += fun(a,b,c,d,e,f,sum);
*(array+t) = sum;
}

其中a,b,c,d,e,fdoubleTint。欢迎任何内容,包括但不限于内存对齐、并行性、openmp/MPI 和 SSE 指令。编译器是标准的 gcc、microsoft 或常用的编译器。如果解决方案是特定于编译器的,请与您的解决方案相关联的特定编译器和任何选项标志。

谢谢!

PS:忘了提到属性fun。请假设它是一个内部没有循环的简单函数,仅由基本的算术运算组成。简单地把它想象成一个内联函数。

EDIT2:由于fun的细节很重要,请忘记参数c,d,e,f并假设fun被定义为

inline double fun(a,b, sum){
return sum + a* ( b - sum);
}

【问题讨论】:

  • 要优化,您通常需要知道某段代码值得优化。在不知道 fun(...) 的作用或 T 在实际中可以得到多大的情况下,这并不是一个真正有用的问题。
  • 函数调用开销将超过任何循环优化(除非它是内联的)。您正在执行微优化而无需先进行测量。
  • 为什么使用*(array+t) 而不是等效但更清晰的array[t]
  • 编写不可读的代码应该让你被解雇。 叹息
  • 此代码是否已被分析?你知道循环是问题吗?你知道 fun() 不是问题吗?分析代码后,您将能够更好地进行优化。如果您不进行概要分析,那么您的“优化”就是 SWAG。

标签: c++ c performance optimization loops


【解决方案1】:

由于sum 以一种不平凡的方式依赖于其先前的值,因此不可能并行化代码(因此 OpenMP 和 MPI 已被淘汰)。

应通过适当的编译器设置自动强制/使用内存对齐和 SSE。

除了内联fun 和展开循环(通过在-O3 中编译)之外,如果fun 完全通用,我们无能为力。


由于fun(a,b,sum) = sum + a*(b-sum),我们有封闭的形式

            ab             t+1
array[t] = ———— ( 1 - (2-a)    )
            a-1 

可以矢量化和并行化,但除法和求幂可能非常昂贵。

尽管如此,使用封闭形式,我们可以从任何索引开始循环,例如创建 2 个线程,一个从 t = 0 到 T/2-1,另一个从 t = T/2 到 T-1,它们执行原始循环,但初始 sum 是使用上述封闭形式解决方案计算的。此外,如果只需要数组中的几个值,则可以延迟计算。

对于SSE,你可以先用(2-a)^(t+1)填充数组,然后将x :-&gt; x - 1应用于整个数组,然后将x :-&gt; x * c应用于c = a*b/(1-a)的整个数组,但可能有@ 987654321@ 已经。

【讨论】:

  • 如果他没有告诉我们 fun(a,b,sum) 的作用,你怎么能代数操作它?
  • 最好把一些数字放在“非常昂贵”上——通常,我发现除法的成本是乘法的 5 倍,取幂的成本是乘法的 50 倍。
  • 请注意,a-1 是一个常量,也是一个双精度数。因此1/(a-1) 可以预先计算。取幂是 t 的幂,它是常数,因此需要 O(log t)。尽管如此,整个计算仍然是 O(T log T)。
【解决方案2】:

除非 fun() 非常微不足道 - 在这种情况下考虑内联,它可能会支配你可以对循环执行的任何其他操作。

你可能想看看 fun() 中的算法

【讨论】:

    【解决方案3】:

    可以做的一个(非常)小的优化是:

    double sum = 0, *array;   
    array = (double*) malloc(T * sizeof(double));
    double* pStart = array;
    double* pEnd = array + T;
    while(pStart < pEnd)
    {
        sum += fun(a,b,c,d,e,f,sum); 
        *pStart++ = sum; 
    }
    

    这消除了循环的每次迭代将 t 添加到数组中,t 的增量被 pStart 的增量替换,对于小迭代集(认为小于 3,在这种情况下应该取消循环) ,没有真正的收获。编译器应该自动执行此操作,但有时需要一点鼓励。

    还取决于 T 的大小范围,可以通过使用可变大小的数组(将被堆栈分配)或对齐的 _alloca 来获得性能

    【讨论】:

    • 许多处理器实际上并没有在存储之前添加 T,它们使用寄存器间接执行添加,同时在时钟周期 str r1,[r2,r3] 内执行指令。所以可以洗。一些处理器被破坏的移动指针更快
    • 当然,您可以执行 str r1,[r2],#8(以 ARM 为例)并在寄存器间接占用两个指令的一条指令中执行销毁指针及其增量。通常,除非您针对特定处理器,否则指针解决方案和索引数组解决方案在清洗性能方面是明智的,索引数组解决方案对于大多数人来说更易于阅读和维护。我们所做的只是在这里节省了两条指令,fun() 加上调用的设置将支配所消耗的时间。
    【解决方案4】:

    上面的代码几乎是你能做的最快的。

    • 无论如何,malloc 通常可以很好地处理内存对齐。
    • 代码无法并行化,因为f 是先前总和的函数(因此您不能将计算分解为块)。
    • 未指定计算,因此不清楚 SSE 或 CUDA 或类似的东西是否适用。
    • 同样,您不能基于f 的属性执行任何有用的循环展开,因为我们不知道它的作用。

    (从风格上讲,我会使用array[t],因为它更清楚发生了什么,而且速度不会变慢。)


    编辑:现在我们有了f(a,b,sum) = sum + a*(b-sum),我们可以尝试手动展开循环,看看是否有某种模式。就像这样(我用** 来表示“权力”):

    sum(n) = sum(n-1) + sum(n-1) + a*(b-sum(n-1)) = (2-a)*sum(n-1) + a*b
    sum(n) = (2-a)*( (2-a)*sum(n-2) + a*b ) + a*b
    . . .
    sum(n) = a*b*(2-a)**n + a*b*(2-a)**(n-1) + ... + a*b
    sum(n) = a*b*( (2-a)**0 + (2-a)**1 + ... + (2-a)**n )
    

    嗯,现在,是不是很有趣!我们已经从循环公式转换为几何级数!而且,您可能还记得几何级数

    SUM( x^n , n = 0..N ) = (x**(n+1) - 1) / (x - 1)
    

    这样

    sum(n) = a*b*( (pow(2-a,n+1) - 1) / (1-a) )
    

    现在您已经完成了数学运算,您可以在任何地方开始求和(使用有点昂贵的 pow 计算)。如果你有 M 个空闲处理器,并且你的数组很长,你可以将它分成 M 个相等的部分,使用上面的计算找到第一个总和,然后使用你之前使用的递归公式(使用函数)来填充休息。

    至少,您可以分别计算 a*b 和 2-a 并使用它们来代替现有函数:

    sum = ab + twonega*sum
    

    这将你的内部循环中的数学大约减半。

    【讨论】:

      【解决方案5】:

      接受@KennyTM 的回答。正如他继续表明的那样,他说计算不可并行是错误的。在展示你可以用封闭形式重写你的递归关系时,他说明了优化程序的一个非常普遍的原则——选择你能找到的最好的算法并实现它。其他答案建议的微优化都不会接近计算封闭形式并将计算并行分布在许多处理器上。

      而且,为了避免有人提出这只是学习并行化的一个例子,我认为@KennyTM 的答案仍然适用——不要学习优化代码片段,学习优化计算。为您的目的选择最佳算法,很好地实现它,然后才担心性能。

      【讨论】:

      • +1,但我认为“为您的目的选择最佳算法,实施好,然后才担心性能。”具有误导性。毕竟,我们选择最佳算法的全部原因是因为我们担心性能。
      • 考虑到求幂的成本,如果您以幼稚的方式拆分它,您需要 100 多个处理器才能超过一个处理器的吞吐量。您不想要封闭式表单 - 您想用封闭式表单初始化,但要使用更新速度快 50 倍以上的循环。
      • @Rex - 事实上,我只是认为很多人隐藏在“过早的优化是万恶之源”的口头禅背后,以谴责 所有 优化尝试,而不仅仅是微优化。 @Mark - 只需移植到 CUDA 即可轻松利用数百个内核。
      • @HPM:是的;我在对问题的回答中暗示了这一点,而且我还可以使用 100 多个处理器。但我可能想将我的 100 多个处理器用于需要 100 多个处理器的东西,而不是不需要的东西。 @John:确实,人们也经常躲在“走开并使用分析器”的答案后面。好的建议,是的,但这不是唯一的好建议。
      • @HFM 这篇文章的目的是让我学习低级优化。我的问题中的循环函数关系通常比这更复杂,而具有数学背景的我总是试图为此找到更好的公式。这个问题是通用的,以便人们可以讨论可以使用的低级优化技术。我不是在寻求解决这个特定问题的方法,而是在分析之后可以使用一组技术来制作我的代码。谢谢:)
      【解决方案6】:

      看看 callgrind,valgrind 工具集的一部分。通过它运行您的代码,看看是否有任何问题需要花费异常大量的时间。然后你就会知道需要优化什么。否则,您只是在猜测,而您(以及我们其他人)很可能会猜错。

      【讨论】:

        【解决方案7】:

        只是一些尚未提出的建议。对于现代 PC 风格的处理器,我有点过时了,所以它们可能没有太大的区别。

        • 如果您可以容忍较低的精度,使用float 可能比double 更快。基于整数的定点可能会更快,具体取决于浮点运算的流水线程度。
        • T 倒数到零(并在每次迭代中递增array)可能会稍微快一些 - 当然,在 ARM 处理器上,每个循环会节省一个周期。

        【讨论】:

        • 浮点运算和双重运算在当今的 FP ALU 中通常同样快...节省通常来自更高的缓存命中率 :-)
        • 我在一个不能接受浮动的领域:(
        【解决方案8】:

        另一个非常小的优化是将 for() 变成

        while (--T)

        因为与零比较通常比比较两个随机整数更快。

        【讨论】:

          【解决方案9】:

          我会在编译器上启用向量处理。您可以重写代码以打开循环,但编译器会为您完成。如果是更高版本。

          您可以使用 t+array 作为 for 循环增量...再次优化器可能会这样做。 意味着您的数组索引不会使用再次乘法优化器可能会这样做。

          您可以使用该开关转储生成的汇编代码,并使用该开关查看您可以在代码中更改哪些内容以使其运行得更快。

          【讨论】:

          • funsum 的函数,因此它不能移出循环。
          • 我不禁觉得这是可以优化的地方,只要我们知道它做了什么。
          【解决方案10】:

          按照@KennyTM 的出色回答,我想说按顺序执行的最快方法应该是:

          double acc = 1, *array;
          array = (double*) malloc(T * sizeof(double));
          // the compiler should be able to derive those constant expressions, but let's do it explicitly anyway
          const double k = a*b/(a-1);
          const double twominusa = 2 - a;
          for(int t = 0; t < T; ++t){
              acc *= twominusa;
              array[t] = k*(1-acc);
          }
          

          【讨论】:

            【解决方案11】:

            您可以展开循环并进行一些数据预取,而不是让编译器展开循环。在网络上搜索数据驱动设计 c++。以下是循环展开和预取数据的示例:

            double sum = 0, *array;
            array = (double*) malloc(T * sizeof(double));
            
            // Calculate the number iterations and the
            //    remaining iterations.
            unsigned int iterations = T / 4;
            unsigned int remaining_iterations = T % 4;
            double sum1;
            double sum2;
            double sum3;
            double sum4;
            double * p_array = array;
            for(int t = 0; t < T; T += 4)
            {
                // Do some data precalculation
                sum += fun(a,b,c,d,e,f,sum);
                sum1 = sum;
                sum += fun(a,b,c,d,e,f,sum);
                sum2 = sum;
                sum += fun(a,b,c,d,e,f,sum);
                sum3 = sum;
                sum += fun(a,b,c,d,e,f,sum);
                sum4 = sum;
                // Do a "block" transfer to the array.
                p_array[0] = sum1;
                p_array[1] = sum2;
                p_array[2] = sum3;
                p_array[3] = sum4;
                p_array += 4;
            }
            // Handle the few remaining calculations
            for (t = 0; t < remaining_iterations; ++t)
            {
                sum += fun(a,b,c,d,e,f,sum);
                p_array[t] = sum;
            }
            

            这里最大的亮点是对fun 函数的调用。执行功能时涉及隐藏的设置和恢复指令。此外,该调用会强制执行一个分支,这将导致指令流水线被刷新和重新加载(或导致处理器在分支预测中浪费时间)。

            另一个性能损失是传递给函数的变量数量。这些变量必须放在堆栈中并复制到函数中,这需要时间。

            【讨论】:

            • 我认为循环展开是由编译器今天完成的 :-) 另外,如果您让存储操作交错,您的手动展开仍然具有数据依赖性,可以通过更填充的管道来缓解.
            • 函数可以内联,因此不会对性能产生负面影响。
            【解决方案12】:

            许多计算机的处理器硬件中都有一个专用的multiply-accumulate 单元。根据您的最终算法和目标平台,如果编译器在优化时尚未使用它,您也许可以使用它。

            【讨论】:

              【解决方案13】:

              以下可能不值得,但是....

              例程 fun() 采用七 (7) 个参数。

              如果编译器可以利用以下场景,将参数的顺序更改为 fun (sum, a, b, c, d, e, f) 可能会有所帮助。参数 through 似乎是不变的,并且似乎只在代码中的这个级别发生变化。由于参数在 C/C++ 中是从右到左推入堆栈的,如果通过的参数确实是不变的,那么编译器理论上可以优化堆栈变量的推入。换句话说,through 只需要被压入堆栈一次,理论上可能是循环中唯一被压入和弹出的参数。

              我不知道编译器是否会利用这种情况,但我将它作为一种可能性扔在那里。反汇编可以验证它是真是假,而剖析将表明如果为真,可能会有多大的好处。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-11-13
                • 2016-06-29
                • 1970-01-01
                • 2021-02-26
                • 1970-01-01
                • 2011-07-01
                • 1970-01-01
                相关资源
                最近更新 更多