C++ 运算符的性能表现

5
有没有c++算术运算符之间的性能差异,它们是否都同样快?例如,“++”比“+=1”更快吗?“+=10000”呢?如果数字是浮点数而不是整数,会有显著的区别吗? “*”比“+”慢很多吗?
我尝试对“++”,“+=1”和“+=10000”分别执行10亿次操作。奇怪的是,时钟周期的数量(根据time.h)实际上与直觉相反。人们可能会认为任何最快的都是“++”,然后是“+=1”,然后是“+=10000”,但数据显示出轻微的相反趋势。这种差异在执行10亿个操作时更加明显。这一切都是针对整数的。
我正在涉足科学计算,所以我想测试操作符的性能。例如,如果其中任何一个操作符的操作时间与输入成线性关系。

3
如果非得说有区别的话,我认为 x++ 比 x += 1 更长。不过我预期 ++x 和 x += 1 的性能应该是相等的。如果你不明白其中的区别,建议先去了解一下它们之间的差异,而不是担心编译器是否做出了正确的优化。 - kyoryu
1
对于基本类型,也就是说。 - GManNickG
1
@Thomas:这取决于上下文。y = x++与y = (x += 1)相比,如果编译器为两者生成相同的代码,那么它将不是一个很好的编译器。 - kyoryu
1
@Thomas:y = x++;y = (x += 1); 不是 等价的。y = (x += 1); 的意思是 y = ++x; - Steve Schnepp
此外,++x 的性能不等于 x++,因为 x++ 需要在内部进行对象复制,而 ++x 则不需要。 - Michal W
显示剩余3条评论
11个回答

12
关于您的编辑,语言并未涉及其运行的架构。您的问题与平台有关。
那么,通常所有基本的数据类型操作都与汇编有一一对应关系。例如,x86有一个指令可以将值加1,即i++i += 1。加法和乘法也有单独的指令。
从硬件上来讲,很明显,两个数的相加或相乘至少与它们的位数成线性关系,因为硬件具有恒定的位数,所以时间复杂度是O(1)。
浮点数通常有自己的处理单元,并且还有单独的操作指令。
这重要吗?
为什么不编写能够完成您需要的功能的代码呢?如果想加1,就使用++。如果想加大数,请加上大数。如果需要浮点数,请使用浮点数。如果需要乘以两个数字,请将它们相乘。
编译器会找出最佳方法来实现您的意图,因此不要试图钻空子,只需按照您的需求去做,让编译器来完成艰苦的工作。
在编写好可行的代码后,如果您认为它太慢了,请进行优化并找出原因。您会发现问题不在于乘法和加法,而在于错误的解决整个(子)问题的方式。
在桌面平台上,实际上所有您列举的运算符都将在单个CPU指令中完成。

3
OP从未说明原因,只是提出了问题。你好像在假设最终目的是微观优化,但这可能并不是事实。 - Chris Tonkinson
7
实际上,我有点厌倦这种势利眼的态度,从“Does it matter?”,“Why not…”开始。如果你知道一些包含这些比较的网页,为什么不简单地告诉他呢,因为他很好奇,否则就保持沉默呢? - Viktor Sehr
因为这真的无关紧要。这是如此依赖于平台,我难道应该伸手穿过屏幕为他进行剖析或反汇编吗? - GManNickG
这是务实的现实主义,而不是势利眼。在整个大局中,“x = x +1”、“x ++”和“x += 1”的区别微不足道,可以忽略不计。内存访问、I/O和算法的渐近复杂度将对性能产生更大的影响,而我们正在谈论的微不足道的差异则无关紧要。如果性能真的是一个问题,应该设置一个分析器,它会让你知道哪个是总体上更快的。此外,编译器可以自由地编译任何一个,所以这是一个编译器问题,而不是语言问题。 - kyoryu
2
当然,你使用 ++i 还是 i+=1 都无所谓。我们都知道这一点。但是提问者不知道。这就是他为什么要问的原因。我知道,当我编程时能够忽略这样的性能问题的唯一原因是因为我已经具备了必要的知识来确定“这并不重要”。所以,对于提问者来说,这确实很重要。他需要一些答案来停止担心。 - jalf

6

不,不,是的*,是的*,分别。

*但你真的在意吗?

编辑:为了给出一些现代处理器的概念,你可能可以在进行一次内存访问的时间内完成200个整数加法,而只有50个整数乘法。如果你仔细想想,你大部分时间仍然会受到内存访问的限制。


4
您所询问的是:哪些基本操作会转化为哪些汇编指令,这些指令在我的特定架构上的性能如何。同时这也是您的答案:它们被转换成的代码取决于您的编译器和它对您的架构的了解,它们的性能也取决于您的架构。
请注意:在C++中,运算符可以被重载用于用户定义的类型。它们可能与内置类型不同,并且重载的实现可能是非平凡的(不止一个指令)。
编辑:测试提示。大多数编译器支持输出生成的汇编代码。gcc的选项是-S。如果您使用其他编译器,请查看其文档。

3
最好的答案是与您的编译器同步。

3
查阅您CPU的优化手册。那是您能找到答案的唯一地方。
让您的编译器输出生成的汇编代码。下载您CPU的手册。在手册中查找编译器使用的指令,您就知道它们的性能表现。
当然,这假定您已经了解流水线、超标量乱序CPU的基础知识,以及分支预测、指令和数据缓存等其他内容。请先做好功课。
性能是一个极其复杂的主题。根据上下文,浮点代码可能与整数代码一样快(或更快),也可能慢四倍。通常情况下,分支几乎不会带来惩罚,但在特殊情况下,它们可能会导致严重后果。有时,重新计算数据比缓存它更有效,而有时则相反。
了解您的编程语言。了解您的编译器。了解您的CPU。然后通过分析/计时来检查编译器在您的情况下究竟在做什么,并在必要时通过检查各个指令来进行检查。(并且在计时代码时,请注意所有可能使您的基准测试无效的警告和陷阱:确保启用了优化,但同时确保您要测量的代码没有被优化掉。考虑缓存的影响(如果数据已经在CPU缓存中,则运行速度会更快。如果必须从物理内存中读取,那么需要额外的时间。这两者都可能使您的测量无效,如果不小心的话。请记住您要精确测量什么)
对于您的具体示例,为什么 ++i i += 1快?它们做的是完全相同的事情?有时,添加常量或变量可能会有所不同,但在这种情况下,您在两种情况下都加了一个常量。
总的来说,指令需要固定的常数时间,无论其操作数如何。将某些内容加1的时间与将-2000或1772051912加起来的时间一样长。乘法或除法也是如此。
但是,如果您关心性能,您需要了解整个技术栈的工作原理,而不仅仅是依赖一些简单的经验法则,例如“整数比浮点数快, ++ += 快”(除此之外,这些简单的经验法则几乎从来都不正确,至少不是在所有情况下)。

1

关于您的评估,这里来个小技巧:试试 循环展开。 循环展开是重复在循环中相同语句以减少循环迭代次数。

大多数现代处理器都不喜欢分支指令。 处理器有一个预取指令队列,可以加速处理。 他们真的不喜欢分支指令,因为处理器必须在分支之后清空队列并重新加载。 这比仅处理连续指令需要更多时间。

编码处理时间时,请尽量减少分支数量,这可能发生在循环结构和决策结构中。


许多现代编译器会为您展开循环。 - Donald Hobson

0

根据架构不同,整数算术的内置运算符直接转换为汇编代码(据我所知),++、+=1和+=10000的速度可能是相等的;乘法的速度则取决于平台,重载运算符的速度取决于您自己。


0

C++运算符引起的性能问题并不是来自于运算符本身,也不是来自于运算符的实现。它来自于语法,来自于在你不知道的情况下运行的隐藏代码。

最好的例子是,在一个已经实现了operator[]的对象上实现快速排序,但内部却使用了链表。现在,你将得到O(n^2logn)而不是O(nlogn)[1]。

性能问题的问题在于你无法确切地知道你的代码最终会变成什么样子。

[1]我知道快速排序实际上是O(n^2),但它很少达到这个级别,平均分布将给你O(nlogn)。


0

唐纳德·克努斯“我们应该忘记小的效率问题,大约有97%的时间:过早优化是万恶之源”

除非你正在编写极其时间关键的软件,否则你可能应该担心其他事情。


3
唐纳德·克努斯是万恶之源!尽管这并不完全是他的错,但每个人都利用这句口号作为不理解事物本质的借口。 - Michael Krelin - hacker
2
Knuth忘记的比你所知道的还多。 至于我,也是一样。更重要的是,人们经常会担心微调低效解决方案,而不是将其重构为更有效的解决方案。 优化的bogosort仍然是bogosort。 - kyoryu
1
杰里米,实际上,深入了解事物的本质会让您理解计算机的工作原理。编译器并不是很重要。但我还没有见过有人对微观优化进行贬低而做宏观优化的情况。两者都不能替代另一个。但这个引用只是一种借口。 - Michael Krelin - hacker
1
有很多理由去了解CPU如何执行代码以及它对性能的意义。"了解"并不等同于"过早优化"。"过早优化"是定义上的,指的是当你尝试实际修改程序而不仅仅是理解其行为,并且在你拥有必要数据进行有益修改之前就这样做。 - jalf
1
在进行任何优化(无论是过早的还是其他)之前,您必须对各种代码基元的性能影响有一个基本的了解。 - jalf
显示剩余3条评论

0

简短回答:在测量之前应该打开优化。

长答案:如果您已经打开了优化,正在对整数执行操作,但仍然得到 ++i; i + = 1; 的不同时间,则可能是时候获得更好的编译器了 - 这两个语句具有完全相同的语义,一个称职的编译器应将它们转换为相同的指令序列。


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