【问题标题】:Could this alternative way to loop be more effcient?这种替代的循环方式会更有效吗?
【发布时间】:2011-04-22 21:25:06
【问题描述】:

一个下雨的下午我很无聊,想出了这个:

int ia_array[5][5][5]; //interger array called array

{
        int i = 0, j = 0, k = 0;//counters
        while( i < 5 )//loop conditions
        {
            ia_array[i][j][k] = 0;//do something
            __asm inc k;//++k;

            if( k > 4)
            {
                __asm inc j;  //++j;
                __asm mov k,0;///k = 0;
            }
            if( j > 4)
            {
                __asm inc i;  //++i;
                __asm mov j,0;//j = 0;
            }
        }//end of while
    }//i,j,k fall out of scope

它在功能上等同于三个嵌套的 for 循环。但是在 for 循环中不能使用 __asm 语句。此外,您可以选择不将计数器放在范围内,以便您可以将它们重用于其他循环。我已经查看了两者的反汇编,我的替代方案有 15 个操作码,嵌套的 for 循环有 24 个。因此它可能更快吗?假设我真的要问的是 __asm inc i;比 ++i 更快;?

注意:我无意在任何项目中使用此代码,只是出于好奇。感谢您的宝贵时间。

【问题讨论】:

  • 我怀疑它会更快,但取决于循环内部的内容,它可能会慢很多。如今,编译器可以进行一些很棒的优化,但是在循环中间向它们抛出汇编语句会使它们的生活变得艰难。由于编译器通常没有简单的方法来判断 asm 内部发生了什么,因此很难优化。
  • 如果asm inc i++i快,那你的编译器就需要从后面拿出来拍了。
  • @Mike:对于mov j,0j = 0,我会说同样的话。
  • @damon: +1 @dreamlax: 我同意,我只是在玩 __asm
  • @dreamlax:实际上,你很容易就错了。一个体面的编译器几乎肯定会在寄存器中至少包含 i,并且很可能有 j 和 k,并将它们归零为 xor reg, reg,(或等效地,sub reg, reg),这稍微更有效。

标签: c++ optimization assembly


【解决方案1】:

首先,您的编译器可能会将 i、j 和 k 的值存储在寄存器中。

for (i = 4; i &lt;=0; i--)for(i = 0; i &lt; 5; i++) 更有效,因为 cpu 可以免费确定它执行的最后一次操作的结果是否为零 - 它不必显式与 4 进行比较(请参阅cmovz指令)。

x86 的情况并非如此,必须执行更少的指令会导致更快的代码。指令流水线存在各种各样的问题,对于程序员来说,这些问题很快就会变得难以手工编写。把它留给编译器吧,这些天它们已经足够高效了(虽然绝对不是最优的......但是谁愿意等待几个小时来编译他们的代码)。

您可以通过在每个实现中运行您的函数几十万次来自己检查它,然后检查哪个更快。检查是否可以在 for 循环中编写 asm 指令

__asm {
    inc j;
    mov k, 0;
}

(我已经有一段时间没有这样做了)

附:用 asm 进行试验很有趣,它会非常有趣和有益!

【讨论】:

    【解决方案2】:

    不,它不会更快。事实上,它很容易变慢。你的编译器的优化器在这方面几乎肯定比你更有效。

    【讨论】:

    • 即使在十年前,这也是错误的。在 2011 年,编译器在优化方面比大多数开发人员更聪明。如果你想优化你的代码,展开循环最好留给编译器:通常有更有效的方法来解决这个问题,这些技术将帮助编译器完成它的工作。
    【解决方案3】:

    这将是非常特定于编译器和编译器开关的,但是您的代码将在每个循环迭代中进行三个测试,而传统的嵌套循环在每个最内层循环迭代中只有一个,所以我认为您的方法往往是一般较慢。

    【讨论】:

    • +1:好点,我没有考虑 ifs 所在的循环/内部循环的哪个部分之间的区别。
    【解决方案4】:

    几件事:

    1. 你不能根据输出中操作码的数量来判断汇编代码的速度。编译器可以展开循环以消除分支,并且许多现代编译器将尝试像上面那样对循环进行矢量化。前者的操作码可能比原始代码的操作码多且速度更快,而后者的操作码可能更少且速度更快。

    2. 通过将__asm 语句放入您的代码中,您可能排除编译器可以对循环进行的任何优化。因此,如果您使用诸如英特尔编译器之类的非常快的东西进行编译,那么您的代码可能会比使用编译器获得更差的性能。对于像这里的代码这样简单的东西尤其如此,其中数组大小是静态已知的并且循环边界是恒定的。

    如果您真的想了解编译器能做什么/不能做什么,请拿起一本书或参加有关优化编译器和矢量化的课程。有大量不同的优化,即使是像这样的简单代码在特定架构上的性能也很难理解。

    在许多内核和数字运算代码中,编译器仍然无法比知识渊博人类做得更好,但如果没有大量架构细节方面的经验,你也不会做得比icc -fastxlC -O5

    【讨论】:

    • +1:我知道更少的操作码不一定更快,感谢 icc -fast / xlc -05 我会研究它们
    【解决方案5】:

    虽然在优化方面确实有可能击败编译器,但你不会这样做。您用汇编语言编写的代码非常明显,是机械类型的翻译,任何中等体面的编译器(甚至是非常糟糕的编译器)都可以轻松完成。

    如果您想击败编译器,您需要走得更远,例如重新排列指令以允许更多并行执行(绝对不平凡)或找到比编译器更好的指令序列。

    在这种情况下,例如,您可能至少有机会注意到 iarray[5][5][5] 可以(从汇编语言的角度)被视为 5*5*5 = 125 个元素的单个平面数组,并且将大部分本质上是 memset 的内容编码为一条指令:

    mov ecx, 125    // 125 elements
    xor eax, eax    // set them to zero
    mov di, offset ia_array // where we're going to store them
    rep stosd       // and fill that memory.
    

    然而,实际上,这可能不会是对编译器可能生成的内容的重大(甚至可能是次要)改进。它更有可能接近(至少几乎)跟上所需的最低限度。

    下一步将考虑使用非临时存储而不是简单的stosd。这实际上不会加速这个循环(无论如何),但如果可能已经在缓存中的其他代码可能立即更重要,它可能会通过避免这个存储污染缓存来获得一些整体速度。您还可以使用其他一些 SSE 指令来获得一点速度——但即使在最好的情况下,您也不能期望比其中的几个百分点更好。底线是,为了将一些内存归零,速度主要受总线速度的限制,而不是您使用的指令,因此您所做的任何事情都可能没有多大帮助。

    【讨论】:

    • +1:为了扩展 asm 理论,有什么建议可以阅读吗?
    • @QuantumKarl:如果你想了解优化,Agner Fog 的教程是一个不错的开始。 agner.org/optimize.
    猜你喜欢
    • 2011-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-14
    • 2012-08-30
    • 2021-11-18
    • 2017-01-10
    相关资源
    最近更新 更多