递减式for循环比递增式更有效吗?

3

可能是重复问题:
递减计数比递增计数更快吗?

例如,

for (int i = 0; i < max; i++)
{
    ...
}

并且

for (int i = max-1; i >= 0; i--)
{
    ...
}

这两个循环本质上是相同的,假设循环不包含任何数组操作。然而,对于第一种情况,每次迭代都需要将max加载到处理器寄存器中,然后在i和max之间进行比较。另一方面,后者情况不需要将0加载到寄存器中,因为0已经存在寄存器中,所以后者只需要进行一次比较。如果我错了,请纠正我,并解释我正确的地方。谢谢。


1
我会非常惊讶如果真的有任何差别。但是请不要浪费时间寻找这样微小的东西去为你的应用程序节省几毫秒的时间。 - chris
2
for (int i = max; i--;)for (int i = max-1; i >= 0; i--) 更符合惯用语,因为它适用于无符号索引类型。 - ildjarn
1
对其进行基准测试并告诉我们结果。哦,如果编译器有足够的寄存器并且可以确定它永远不会改变,那么很可能可以避免溢出max - Useless
它们在x86上都是一个汇编指令,所以没有区别。 - Seth Carnegie
@chris:除非这是最热的循环并且应用程序运行了非常非常长的时间,否则我会感到惊讶,如果这种变化会产生几个微秒 - David Rodríguez - dribeas
显示剩余9条评论
4个回答

6

省略号代表的代码几乎肯定会使任何实际性能差异降至噪音水平。但是,你对所有假设并不正确。

每次迭代都需要将max加载到处理器寄存器中,然后在i和max之间进行比较

也许是这样,但可能不是。这取决于你的代码,但是任何合理的优化编译器都能检测出计数器是否在迭代之间发生变化。

我不确定你从哪里得到了一些想法,但它们有点误导,并且没有考虑到优化编译器的工作原理。查看你的反汇编并自己看看真正的区别是什么。哦,该死,我来做吧(这很有趣):

程序为:

int main(int argc, char *argv[]){   
    int max = 10;
    for (int i = max-1; i >= 0; i--)
    {
        cout << i;
    }
    return 0;
}

生成的汇编代码(VS2010版本,以下注释为本人添加)如下所示:
int main(int argc, char *argv[]){   
00341000  push        esi  
    int max = 10;
    for (int i = max-1; i >= 0; i--)
00341001  mov         esi,9               ; move a static 9 into esi
00341006  jmp         main+10h (341010h)  
00341008  lea         esp,[esp]           ; load the address of whatever
0034100F  nop                             ; esp points to in memory 
    {                                     ; (not a memory fetch, just address calculation)
        cout << i;
00341010  mov         ecx,dword ptr [__imp_std::cout (342048h)]  
00341016  push        esi  
00341017  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (342044h)]  
0034101D  dec         esi                 ; decrement counter
0034101E  jns         main+10h (341010h)  ; jump if not signed
    }

对于更通俗易懂的版本...

int main(int argc, char *argv[]){   
00AC1000  push        esi  
    int max = 10;
    for (int i = 0; i < max; i++)
00AC1001  xor         esi,esi  
    {
        cout << i;
00AC1003  mov         ecx,dword ptr [__imp_std::cout (0AC2048h)]  
00AC1009  push        esi  
00AC100A  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0AC2044h)]  
00AC1010  inc         esi               ; increment esi
00AC1011  cmp         esi,0Ah           ; compare to 10 (0Ah)
00AC1014  jl          main+3 (0AC1003h) ; if less, jump to top 
    }

所以,第一个版本使用了jns指令(如果不是有符号数则跳转),因此比较有点简化(与0比较)。它还包含了一些其他指令,但没有比较。然而,请注意,在第二个版本中所做的比较也是静态的。它知道在循环期间max不会改变,因此可以相应地优化该部分。但我要强调一下,这不太可能产生可观的性能优势。即使是我Windows电脑上的高性能计时器也无法给出两者之间的明显统计差异,因为对cout的调用时间比循环指令长得多。

2

编译器级别的优化会根据循环内容的不同而有所不同,因此这可能是一个无用的点。例如,编译器可以推断出Max的值在循环期间不会发生变化,因此只需要将其加载到内存一次。

如果您真的担心这种微观优化水平,您需要确切地知道您计划使用的编译器和编译器设置,然后在目标硬件上进行时间测试以比较不同的选项。或者,您可以直接查看编译器输出,并将实际汇编或机器级指令进行比较,以查看哪个版本使用的指令更多。


1

通常情况下,增量或减量的选择并不基于任何性能相关的因素。它通常基于算法或代码片段的逻辑流程,以最符合循环上下文的方式为依据。

根据实现方式,前置增量(++i)有时可能比后置增量(i++)更快,但如果迭代次数是恒定的,编译器将优化大多数循环甚至完全展开它们。在测试中确定了性能瓶颈之前,通常不值得优化任何代码。

简而言之,不要为小事烦恼。


2
我认为问题不在于前增量与后增量,而在于正向或反向遍历循环计数。 - ildjarn
1
没错,它并没有对答案产生影响。我只是因为楼主对效率感兴趣才加上的。 - AJG85

0

是的,这可能更好,因为与0比较比与非零比较更好。但现代编译器通常会在优化代码方面做得很好,所以不会有太大的区别。

最后一点-这是微小的优化。除非它使代码更易读,否则我会避免使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接