C语言与汇编语言的效率比较

4
下面的汇编代码有多快:
shl ax, 1

与以下C代码相比:

num = num * 2;

我该如何找到答案呢?



4
小心那些说:“编译器会为你优化”的人。编译器只会优化编译器作者告诉它去优化的那些东西。要弄清楚,你需要通过实证测试来判断差异,而不是基于“应该”或“将会”的观点。 - Dave Jarvis
1
@Dave Jarvis,我完全同意。这种观点通常也是由那些认为性能完全无关紧要、认为摩尔定律将不断产生更快的CPU或者根本不知道编译器如何工作的人所支持的。 - BobbyShaftoe
4
@Dave - 你说的没错,但GCC团队已经有30多年时间来添加优化了。如果他们不能优化它,那么要优化它就会相当困难。 - Chris Lutz
2
@Chris:GCC 的开发人员错过了在 gcc 4.3.3 中按二的幂优化求模运算。n % x = n & (x - 1),其中 x 是二的幂次。这不是一个困难的优化,但是对于相同条件,gcc -O3 使用 & 比 % 生成更快的代码。你可以自己试一下。 - Dave Jarvis
9个回答

25

你的汇编代码变体可能更快,也可能更慢。是什么让你认为它一定更快呢?

x86 平台上,有很多方法可以将某个数乘以2。我预计编译器会使用 add ax,ax ,这显然比你的 shl 操作更有效率,因为它不涉及潜在的常量存储(在你的情况下是“1”)。

此外,在 x86 平台上,很长一段时间里,将常量乘以其他数的首选方式并不是移位操作,而是一个 lea 操作(如果可能的话)。在上面的示例中,即 lea eax,[eax*2] 。(通过 lea eax,[eax*2+eax] 可以将一个数乘以3)

对于移位操作被认为更“快”的信仰已成为新手的一则美好的老故事,几乎无关紧要。并且,通常情况下,如果你的编译器是最新的,那么它对底层硬件平台的了解要比那些对移位操作有天真爱好的人更为深入。


只是为了增加混乱,如果num存储在ax寄存器中,移位可能是编译器生成的;如果num没有存储在ax寄存器中,则汇编程序不会像C语言那样执行相同的操作;您必须将值放入正确的寄存器中,进行移位,然后再次存储结果。 - Jonathan Leffler
不仅取决于编译器的实现方式,还取决于变量是全局的还是局部的以及它的使用方式。例如,如果您仅临时将值用作数组索引,则C编译器可能使用寻址模式进行计算,甚至避免生成任何指令。 - Adisak

8

请问这是一个学术问题吗?我猜你明白这属于“剪头发减肥”的一般类别。


只是现在才看到,呵呵。这只是一个纯粹的学术“兴趣”问题,绝对没有任何实际生活相关性。 - Kyle Rosendo

5
如果您正在使用GCC,请使用选项-S要求查看生成的汇编代码。您可能会发现它与汇编指令相同。
回答原始问题,在乱序处理器上,指令速度由吞吐量和延迟测量,您可以使用rdtsc汇编指令来测量两者。但其他人已经为许多处理器完成了此工作,因此您不需要费心PDF

4
在大多数情况下,这不会有任何区别。在几乎所有现代硬件上,乘法速度都很快。特别是,通常足够快,除非您精心优化了代码,否则流水线将隐藏整个延迟,您将看不到两种情况之间的速度差异。
当您单独执行乘法和移位时,您可能能够测量性能差异,但通常在编译代码的其余上下文中不会有任何差异。(如我所述,如果代码经过精心优化,则可能不成立)。
现在,话虽如此,移位仍然比乘法快得多,并且几乎任何合理的编译器都会将固定的二次幂乘法映射到移位(假设语义在目标架构上实际上是等效的)。
编辑:如果您真的关心这个问题,还可以尝试x+x。 我知道至少有一种体系结构,这比移位更快,具体取决于周围的上下文。

有趣啊。你还记得在哪个处理器上 x+x 更快一些,而不是 x<<1 吗?如果这是 ia-32 / amd64,那么它是编译为 add 还是 lea 的呢? - Pascal Cuoq
2
不是x86处理器。这是一个嵌入式处理器,其中移位器在加法器之后运行一个周期,并且当加法的结果被移位器使用时会出现一个周期的停顿。 - Stephen Canon
许多处理器具有更多的功能单元可以执行加法,而不是可以执行移位的功能单元,因此即使它们具有相同的延迟,使用加法也会有所帮助(可能具有更好的ILP)。话虽如此,你很幸运才能注意到任何差异。 - Keith Randall
实际上,在X86上,移位有时比加法更昂贵。在Pentium时代,移位和旋转只能在两个流水线中的一个运行,因此,如果您尝试在同一时钟周期内安排两个独立的移位,则其中一个将停顿。加法没有这种限制。有关详细信息,请参见Michael Abrash的《黑书》或《代码优化之道》。 - Adisak

3
答案取决于很多因素,正如你在这里看到的那样,编译器对你的C代码的处理取决于很多因素。如果我们谈论x86-32平台,则以下内容应该是普遍适用的。
在基本层面上,你的C代码指示了一个内存变量,至少需要一条指令才能将其乘以2:"shl mem,1"。在这种简单情况下,C代码将更慢。
如果num是局部变量,则编译器可能会决定将其放在寄存器中(如果它被足够频繁地使用和/或函数足够小),然后你将拥有你的“shl reg,1”指令,也可能不会。
哪个指令最快与处理器中的实现方式有关。Shl可能不是最佳选择,因为它会影响C和Z标志,从而使它变慢。几年前的建议是“lea reg,[reg + reg]”(所有reg都相同),因为lea不会影响任何标志,并且存在各种变体,例如(以x86-32平台上的eax寄存器为例):
lea eax,[eax+eax]    ; *2
lea eax,[eax+eax*2]  ; *3
lea eax,[eax+eax*4]  ; *5
lea eax,[eax+eax*8]  ; *9

我不知道现在的规范是什么,但你的编译器可能会知道。

至于测量,在这里搜索有关rdtsc指令的信息,它是最好的替代方案,因为它计算实际时钟周期。


3

如果你有一个不错的编译器,它会生成相同或类似的代码。最好的方法是反汇编并检查创建的代码。


同意。如果代码有显著不同,那么再进行基准测试和检测统计学显著性的麻烦。 - Paul Nathan

1

将它们放入一个循环中,使用计数器使其运行时间至少为一秒钟。使用您喜欢的计时机制来查看每个操作所需的时间。

汇编测试应该在同一个C程序中使用内联汇编完成,以便与纯C测试进行比较。

顺便说一句,我认为您应该添加第三个测试:

num <<= 1;

问题是它是否与汇编版本执行相同的操作。

1
如果针对你的目标平台,将数字左移是最快的乘以二的方法,那么编译器在编译代码时很可能会这样做。查看反汇编码以进行检查。
因此,对于那一行代码来说,它可能是完全相同的速度。但是,由于你不太可能只有一个包含该行的函数,你很可能会发现编译器会推迟移位直到值被使用,或者以其他方式将其与周围的代码混合在一起,使其变得不那么明确。一个好的优化编译器通常可以很好地打败差到平均水平的手写汇编。

0
如果现在的编译器(vc9)真的做得很好,它应该能够大幅度地超越vc6,但事实并非如此。这就是为什么我甚至更喜欢使用VC6来编写一些比使用mingw和-O3编译的代码运行更快的代码以及使用VC9和/Ox编译的代码。

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