这种替代的循环方式能更高效吗?

3

一个下雨的下午,我感到无聊,于是想出了这个东西:

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;更快吗?

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


6
我怀疑它不会更快,但根据循环内部的内容,它可能会慢得多。如今编译器可以进行一些极好的优化,但在循环中插入汇编语句会使它们的生命变得艰难。由于编译器通常无法轻松判断 asm 内部发生了什么,因此它在优化时遇到了困难。 - Damon
6
如果 asm inc i++i 更快,那么你的编译器需要被带到后面枪毙。 - Mike Seymour
@Mike:我会对mov j,0j = 0说同样的话。 - dreamlax
@damon: +1 @dreamlax: 我同意,我只是在玩__asm。 - QuantumKarl
@dreamlax:实际上,你可能完全错了。一个不错的编译器几乎肯定会将至少i放在寄存器中,而很可能还有j和k,并且会使用“xor reg,reg”(或等效的“sub reg,reg”)将它们清零,这样会稍微更有效率一些。 - Jerry Coffin
显示剩余2条评论
5个回答

2

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

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

对于x86来说,并不是要执行更少的指令就会导致更快的代码。有各种问题与指令流水线有关,这些问题很快就超出了程序员手动编写的范围。把它留给编译器,它们现在已经足够高效了(虽然肯定不是最优的...但是谁想等待数小时才能编译他们的代码呢)。

你可以通过运行每个实现几十万次并检查哪一个更快来自行检查。检查是否可以在for循环中编写asm指令。

__asm {
    inc j;
    mov k, 0;
}

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

附注:尝试使用汇编语言进行实验,它可能非常有趣且富有成效!


1

不,它不会更快,甚至可能会更慢。实际上,你的编译器优化器比你更有效。


即使是十年前,这也是错误的。在2011年,编译器在优化方面比大多数开发人员更聪明。如果您想优化您的代码,展开循环最好交给编译器处理:通常有更有效的方法来解决这个问题,这些技术将帮助编译器发挥其作用。 - user439793

1

这将非常依赖于编译器和编译器开关,但是您的代码每个循环迭代将有三个测试,而传统的嵌套循环每个最内层循环迭代只有一个测试,因此我认为您的方法通常会更慢。


+1:好观点,我没有考虑到if语句所在的循环/内部循环的区别。 - QuantumKarl

1

几件事情:

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

  2. 通过在代码中放置__asm语句,您可能会排除编译器对循环所做的任何优化。因此,如果您使用像英特尔编译器这样的快速编译器进行编译,则您的代码可能比编译器性能差。对于像这里的简单代码,其中数组大小已知静态和循环边界是常数,这一点尤其正确。

如果您真的想了解编译器可以/不能做什么,请阅读一本书或参加一门关于优化编译器和矢量化的课程。有大量不同的优化,即使是在特定架构上运行的简单代码的性能也可能很微妙。

有很多内核和数字计算代码,编译器仍然无法比得上“有经验的”人类,但如果没有足够的架构细节经验,你不会比icc -fastxlC -O5更好。


我知道指令越少不一定越快,感谢你提供的 icc -fast / xlc -05,我会研究一下。 - QuantumKarl

1

虽然在优化方面打败编译器确实是可能的,但你不会用这种方式做到。你用汇编语言编写的代码非常明显,是任何半路出家的编译器(甚至是相当糟糕的编译器)都可以轻松完成的机械类型转换。

如果你想打败编译器,你需要更进一步,比如重新排列指令以允许更多指令并行执行(明显非常困难),或者找到比编译器更好的指令序列。

例如,在这种情况下,你可以至少有一个机会,因为从汇编语言的角度来看,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:关于汇编理论的扩展,有什么推荐的阅读材料吗? - QuantumKarl
1
@QuantumKarl:如果你想学习优化方面的知识,Agner Fog的教程是个不错的开始。http://www.agner.org/optimize/。 - Jerry Coffin

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