循环变量条件的C循环优化

时间:2011-07-12 22:22:57

标签: c optimization loops

如果在档案中询问,请道歉。我发现了一些类似的问题,但没有一个看起来正是我想要的。

我正在研究的问题的提炼版本如下。我有一系列要执行的计算,它们将值存储在4个(非常大的)数组中:A,B,C和D.这些计算是相互依赖的,例如计算b [i]可能需要使用[i-1]。我能够在一个循环中表达所有内容,但这导致边缘情况,对于某些i值,只应执行一些计算。例如:

for(i=0;i<end;i++)
{
    if(i == 0)
        //calculate A[i+1] and B[i+1]
    else if (i == end-1)
        //calculate C[i-1] and D[i-1]
    else
        //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}

对于性能问题,我想避免在我的循环中有条件。与计算相比,评估条件是便宜的,但可能不可忽略。我的问题是编译器是否可以可靠地将其扩展为

//calculate A[1] and B[1]
for(i=1;i<end-1;i++)
{
    //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}
//calculate C[end-2] and D[end-2]

我从档案馆收集到,如果条件表达式是常量的话,编译器会破坏我的循环,但是在这里它们依赖于i,原则上可以通过我的一些计算来改变。它是否会检测到我没有篡改迭代变量,从而以合理的方式将其分开?

额外信息,如果您决定通过建议更好的方式来回答问题:

最初代码是用4个循环编写的,用于计算每个数组的元素。这是编写代码最直观的方法,但效率很低。由于计算一个数组中的元素取决于其他数组中的元素,这意味着我必须在4个循环中的每个循环中从内存中读取所有4个数组。由于这些数组不适合缓存,这不是最佳的,我需要的代码只能循环遍历我的数组一次。

我也知道我可以手动分开我的循环,事实上这就是目前的事情。然而,这些计算涉及非平凡的公式(并且我无法承受在此循环的每次迭代期间调用函数的性能损失),因此分解代码导致代码重复,这不仅非常难以阅读,而且几乎无法维护下一个时间我的公式得到调整(他们会...)

提前致谢!

4 个答案:

答案 0 :(得分:3)

从更广泛的意义上回答你的问题:当优化至关重要时,探查器就是你的朋友。众所周知,开发人员很难猜测处理器在大部分时间内处理代码的位置。分析器将向您显示“昂贵”操作的确切位置,因此您可以集中精力修复将为您带来最显着改进的区域。

我很好奇你的陈述“你不能承受在这个循环的每次迭代中调用一个函数的性能......”你怎么知道的?许多现代处理器都针对函数调用进行了优化,特别是如果您可以传递指针(到struct?)而不是许多单独的参数。如果你的计算确实“非常重要”,那么函数调用的开销可能可以忽略不计。

要考虑的其他事项:

  • 作为实验,重新为您的计算编制索引,使其尽可能在i本身,而不是i-1i+1上运行。因此,例如,使用A[i]B[i]C[i-2]D[i-2]。如果使用优化编译器取得了显着进步,我会感到惊讶,但你永远不会知道......

  • 预先计算任何可能的内容。

  • 尝试将计算分解为常数或常见的较小组件,正如James Greenhalgh建议的那样,因此可以重复使用。

  • 你能更有效地重写方程式吗?对数学的分析可能会引导您走捷径:也许您可以用封闭的形式重写一些(或所有)迭代。

  • 你能用更简单的东西完全取代你的方程式吗?例如,假设您需要按照距离房屋的距离对一组位置进行排序。距离计算需要减法,平方,加法和平方根。通常,平方根是迄今为止最昂贵的操作。但是如果只需要相对距离,则可以完全跳过平方根:按距离的平方排序会生成相同的排序顺序!

  • 如果无法内联,您可以将功能(或其组件)定义为高效宏,那么至少可以避免重复代码吗?正如您所提到的,剪贴板继承是可维护性的致命敌人。

如果没有别的,通过本练习将教你如何编译和C语言的工作方式。祝你好运!

答案 1 :(得分:0)

除了分析之外,我建议查看编译器实际发出的代码(cc -S *.c用于许多编译器)。这应该告诉您如何(或如果)展开循环,以及显示正在移动哪些循环不变量。不要忘记指定与常规编辑相同的优化设置。

答案 2 :(得分:0)

  

我的问题是编译器是否可以将其可靠地扩展为......

你要找的答案是否定的。编译器优化被大大高估,特别是如果您不使用MSVC并定位Wintel。

如前所述,检查汇编器输出以供自己检查。我个人认为安全,只是编写代码,所以编译器不必为你工作 - 事实上,如果你需要做一些特殊的第一次和最后一次迭代重构循环,我认为它更具可读性无论如何......不需要考虑条件及其含义,执行顺序遵循代码编写的顺序,这更自然(和书面语言一样)。

现在,您似乎认为这是不可读的 - 正确的解决方案是不要将所有代码都放在循环中,而是将不可读的代码块移动到具有强大内联提示的良好命名函数中(__forceinline等) ,那么迭代可能如下所示:

prepareForIteration( A, B );
for( int i = 1; i < ( end - 1 ); ++i )
{
    iterationStep( i, A, B, C, D );
}
finaliseIteration( end, C, D );

当然,既然你知道代码实际上做了什么,我相信你能找到更好的名字......

答案 3 :(得分:-1)

你能做的最好的事情就是你已经做过的事情:同时遍历所有四个阵列,而不是分别通过每个阵列。在处理大型数组时,缓存友好的访问模式是迄今为止最重要的微优化。

对于您给出的示例,如果您使用gcc或clang-llvm,我会尝试以下操作:

for(i=0;i<end;i++)
{
    if(__builtin_expect((i == 0), 0))
        //calculate A[i+1] and B[i+1]
    else if (__builtin_expect((i == end-1), 0))
        //calculate C[i-1] and D[i-1]
    else
        //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}

这是编译器的“提示” - 它将传递给CPU - 以帮助进行分支预测。在现代CPU上,错误预测的分支可能需要花费数百个周期。 (另一方面,在现代CPU上,根据过去的频率来预测每个分支的逻辑是非常复杂的。所以你的里程可能会有所不同。)正确预测的分支成本是1个周期甚至更少,所以这是我要担心的下一件事。

请注意,循环展开等经典的“优化”技术几乎肯定是毫无价值的,甚至可能适得其反。

最后,如果您的算法可以进行矢量化,那么这就是您的下一步。将条件完全移出循环的优点是它可以使编译器更容易自动向量化它。但是如果循环不重要,你可能需要手工编写矢量化代码;尝试搜索“SSE内在函数”。

正如其他人所建议的那样,首先使用分析器和-S选项(或等效选项)给汇编程序。祝你好运。