如果不使用结果值,i++和++i之间是否存在性能差异?
- 放松点,仅仅因为你不理解理论基础不会使理论基础变得荒谬或毫无意义。人们反对这个问题,因为理解答案有助于他们更好地理解C语言。答案还显示了一种有趣的方式来证明这两段代码在语义上是相同的。
执行摘要:没有。
由于i的旧值,因此i++可能比++i慢。可能需要保存以备日后使用,但实际上所有现代编译器将对此进行优化。
我们可以通过查看此函数的代码来演示这一点,都有++i和i++。
1 2 3 4 5 6 7 8 9 10
| $ cat i++.c
extern void g(int i);
void f()
{
int i;
for (i = 0; i < 100; i++)
g(i);
} |
除++i和i++外,其他文件相同:
1 2 3 4 5
| $ diff i++.c ++i.c
6c6
< for (i = 0; i < 100; i++)
---
> for (i = 0; i < 100; ++i) |
我们将编译它们,并获得生成的汇编程序:
1 2
| $ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c |
我们可以看到生成的对象文件和汇编程序文件都是相同的。
1 2 3 4 5 6 7
| $ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e
$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22 |
- 我知道这个问题是关于C的,但是我想知道浏览器是否可以为JavaScript进行这种优化。
- 即使编译器没有优化,您也不太可能注意到任何现代CPU上的差异。
- 所以对于您测试过的那个编译器,"no"是正确的。
- @安德烈亚斯:好问题。我曾经是一个编译器编写者,有机会在许多CPU、操作系统和编译器上测试这段代码。我发现的唯一一个没有优化i++案例的编译器(事实上,让我专业地注意到这一点的编译器)是WaltBilofsky的软件工具c80编译器。该编译器适用于英特尔8080 CP/M系统。可以肯定地说,任何不包含这种优化的编译器都不是通用的。
- 另外,no只对整数进行测试,但问题没有提到i的类型。然后你在第1行的概括。抱歉,- 1。
- 尽管性能差异可以忽略不计,并在许多情况下进行了优化-请注意,使用++i而不是i++仍然是良好的做法。完全没有理由不这样做,而且如果您的软件通过了一个不优化它的工具链,那么您的软件将更加高效。考虑到键入++i和键入i++一样容易,因此一开始确实没有理由不使用++i。
- @安德烈亚斯:大多数编译器都会对此进行优化。如果编译器不在任何地方使用它的返回值,那么使用i++是没有意义的。然而,同样的逻辑也应该一直传播到程序员。依赖优化本身就是一个问题。
- @Monokrome作为开发人员可以在许多语言中为前缀和后缀操作符提供自己的实现,如果不首先比较这些函数,这可能不是编译器可以接受的解决方案,这可能是非常重要的。
- @pickypg,对语言差异有很好的理解。Monokrome的建议对C来说是合理的,这就是这个特定问题所要解决的问题。
- @马克哈里森同意了。我只是从一个不同的问题中找到了自己的方式,不希望人们以这个评论线索中的第一个问题为基础提出的假设来阅读本文。
- @请注意这里的问题。这个问题是关于C的,所以你的观点在这里的讨论中是无效的。关于C的一个好处是它不支持运算符重载。
- 当然,我相信对于任何非蹩脚的C编译器来说,答案是否定的。但是,由于许多做C编程的程序员也做C++编程,所以默认情况下使用++i是首选。只是要保持的一个好习惯,即使它在C语言中是毫无意义的。
- 在Ubuntu 14.04 64位上,.o文件(resp.*.s文件)校验和是不同的,即使我们使用-o3选项强制优化。当然,这并不能证明相同的装配代码是不同的。
- 它仍然被称为优化。出于某种原因,即使使用零优化标志也可以完成,但这与实现一个不做任何事情的语句(例如i;语句)完全不同,因为在使用postfix运算符时,底层代码"should"至少要复制一个寄存器。
- @Monokrome的观点很好,我同意。尤其是关于该逻辑应该如何向下传播到程序员。++i应该违约的另一个原因是,更容易看到一些东西正在增加。以reallylongvariablename++;和++reallylongvariablename;为例。更容易看到第一个正在增加(例如,当它自己在一条线上时)。它更符合你在头脑中阅读的方式,"增量i"->"+i"。
- 我用一个使用size_t的循环重复了上述场景,我的散列值不同。在Ubuntu 16.04上运行G++5.3.1。
- @Stevenwalton对.s文件进行了区分,并查看它们的不同之处
- diff i++.s ++i.s只显示文件名和顶部的1c1。比较cpp文件只显示了i++和++i的不同之处。我做了一个小测试,发现我的++i有点快。但是总的运行时间大约是3秒。每次运行10次之后,我的平均值是3.3153,而我的平均值是3.3248。当循环移动到约30秒时,看到了相同的相对差异。一点也不差。
型
从Andrew Koenig的效率与意图来看:
First, it is far from obvious that ++i is more efficient than i++, at least where integer variables are concerned.
号
以及:
So the question one should be asking is not which of these two operations is faster, it is which of these two operations expresses more accurately what you are trying to accomplish. I submit that if you are not using the value of the expression, there is never a reason to use i++ instead of ++i, because there is never a reason to copy the value of a variable, increment the variable, and then throw the copy away.
号
因此,如果不使用结果值,我将使用++i。但不是因为它更有效:因为它正确地表达了我的意图。
- 别忘了其他一元运算符也是前缀。我认为我是使用一元运算符的"语义"方法,而我++是为了满足特定的需要而存在的(添加前的评估)。
- 如果不使用结果值,则语义上没有区别:即,没有优先选择一个或另一个构造的基础。
- 作为一个读者,我看到了不同。所以作为一个作家,我会选择其中一个来表达我的意图。这只是我的一个习惯,尝试通过代码与我的程序员朋友和队友交流:)
- 为什么i++创建一个副本,而不是简单地将增量推迟到引用变量的表达式完成之后?以前的编译器不够聪明吗?
- 这很容易造成原因,当它没有太大的区别,这是自行车脱落,本质上。我敢肯定有人会说我++更可取,因为我看起来像+I,而对于初学者,++我可能只会得到一个奇怪的外观,但我++会让他们打开关于运算符的章节。
- 按照Koenig的建议,我用同样的方式对i += n或i = i + n进行编码,即以目标动词对象的形式,目标操作数在动词运算符的左侧。在i++的情况下,没有右宾语,但规则仍然适用,将目标保持在动词运算符的左侧。
- 如果您试图增加i,那么i++和++i都正确地表达了您的意图。没有理由比另一个更喜欢一个。作为一个读者,我看不出有什么不同,当你的同事看到i++并认为,这可能是一个打字错误,他不是有意增加i的时候,他们会感到困惑吗?
- 这是有区别的。考虑一下:1)int i=0;2)int x=i++;3)int y=++i;从语义上看,第2行说:"给我i的值,然后递增它。";而第3行说"递增i,然后再给我它的值。"
- @伊法林,我们说的是Standalone。没有一个头脑正常的人使用i++或++i作为陈述的一部分。
- 如果你试图"增量i",那么读"增量i"比读"i增量"自然得多。所以即使忽略编译器优化。它正在优化可读性。
一个更好的答案是,++i有时会更快,但从不慢。
每个人似乎都假设i是一个常规的内置类型,比如int。在这种情况下,没有可测量的差异。
但是,如果i是复杂类型,那么您可能会发现一个可测量的差异。对于i++,您必须在递增之前复制您的类。根据拷贝中涉及的内容,它确实可能会变慢,因为使用++it,您只需返回最终值。
1 2 3 4 5 6
| Foo Foo::operator++()
{
Foo oldFoo = *this; // copy existing value - could be slow
// yadda yadda, do increment
return oldFoo;
} |
另一个区别是,对于++i,您可以选择返回引用而不是值。同样,根据复制对象所涉及的内容,这可能会更慢。
使用迭代器是一个现实世界中出现这种情况的例子。复制迭代器在您的应用程序中不太可能是一个瓶颈,但是在不影响结果的情况下,养成使用++i而不是i++的习惯仍然是一个好的实践。
- 问题明确地说明了C,没有引用C++。
- 这个(公认的)问题是关于C,而不是C++,但我想也值得一提的是,在C++中,即使一个类实现了后缀和前缀操作符,它们也不一定是相关的。例如,bar++可以增加一个数据成员,而++bar可以增加一个不同的数据成员,在这种情况下,由于语义不同,您也没有选择使用这两个成员。
- 1,虽然这是C++程序员的好建议,但它并没有回答问题,这是最简单的。在C语言中,使用前缀或后缀完全没有区别。
- @Pacrier问题标记为C,仅C。你为什么认为他们对此不感兴趣?考虑到它是如何工作的,假设它们只对C感兴趣,而不是Java、Cype、COBOL或任何其他非主题语言,这不是明智之举吗?
如果你担心微观优化,这里有一个额外的观察。递减循环"可能"比递增循环更有效(取决于指令集体系结构,如ARM),前提是:
1
| for (i = 0; i < 100; i++) |
在每个循环上,您将有一条指令,分别用于:
在i中加入1。
比较i是否小于100。
如果i小于100的条件分支。
而递减循环:
1
| for (i = 100; i != 0; i--) |
循环将为以下各项提供指令:
减少i,设置CPU寄存器状态标志。
一种取决于CPU寄存器状态的条件分支(Z==0)。
当然,这只在递减为零时才有效!
从ARM系统开发人员指南中记起。
- 好的。但这是否会减少缓存命中率?
- 代码优化书籍中有一个奇怪的老把戏,它将零测试分支和增量地址的优势结合在一起。下面是高级语言中的示例(可能没用,因为许多编译器足够聪明,可以用效率较低但更常见的循环代码替换它):int a[n];for(i=-n;i;++i)a[n+i]+=123;
- @noop你能详细说明你所指的代码优化书籍吗?我一直在努力寻找好的。
- 这个答案根本不是问题的答案。
- @Mezamorphic for x86我的资料来源是:英特尔优化手册,Agner Fog,Paul Hsieh,Michael Abrash。
- @noop:循环迭代中的任何效率增益不会因为a[N+i]而不是a[i]所需的额外工作而丢失吗?
- @如果n不变,那么softwareMonkey在英特尔体系结构上同时生成一条指令。另外,如果n不是常量,您只需将[n+i]替换为一个_-end[i],其中int*a_-end=a+n。由于a+n的值在循环体内部没有更改,优化器可能会为您执行此替换。
从Scott Meyers中获取叶,更有效的C++项目6:区分前缀和后缀形式的增量和减量运算。
在对象方面,尤其是在迭代器方面,前缀版本总是优于后缀版本。
如果您查看操作符的调用模式,那么会出现这种情况的原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // Prefix
Integer& Integer::operator++()
{
*this += 1;
return *this;
}
// Postfix
const Integer Integer::operator++(int)
{
Integer oldValue = *this;
++(*this);
return oldValue;
} |
看看这个例子,很容易看出前缀操作符总是比后缀更有效。因为使用后缀时需要临时对象。
这就是为什么当您看到使用迭代器的示例时,它们总是使用前缀版本。
但是正如您所指出的,对于int,实际上没有什么区别,因为编译器的优化可以发生。
- 我认为他的问题是针对C的,但是对于C++来说,你是完全正确的,而且C人们也应该采纳它,因为他们也可以使用C++。我经常看到C程序员使用后缀语法;-)
- 1,虽然这是C++程序员的好建议,但它并没有回答问题,这是最简单的。在C语言中,使用前缀或后缀完全没有区别。
- @根据安德烈亚斯的回答,lundin前缀和postifx在c中很重要。你不能假设编译器会优化掉,这对于任何一种语言来说都是一个很好的实践,它比我更喜欢使用++I。++
- @jprogrammer他们并不在乎你真诚的回答:)如果你发现他们产生了不同的代码,那可能是因为你未能启用优化,或者是因为编译器不好。不管怎么说,你的答案与话题无关,因为问题是关于C的。
简短回答:
在速度方面,i++和++i之间没有任何区别。在这两种情况下,一个好的编译器不应该生成不同的代码。
长回答:
其他所有答案都没有提到的是,++i与i++之间的差异仅在所发现的表达式中有意义。
在for(i=0; i的情况下,i++在它自己的表达式中是单独的:在i++之前有一个序列点,在它之后有一个序列点。因此,生成的唯一机器代码是"由1增加i",并且它很好地定义了与程序其余部分相关的顺序。所以,如果你把它改成前缀++,一点也不重要,你只需要得到机器代码"增加i"加上1。
++i和i++之间的差异仅在array[i++] = x;和array[++i] = x;等表达中起作用。有些人可能会争辩说,在这样的操作中,后缀会变慢,因为i所在的寄存器必须稍后重新加载。但是,请注意,编译器可以自由地以任何方式对您的指令进行排序,只要它不象C标准所说的那样"破坏抽象机的行为"。
因此,虽然您可以假定array[i++] = x;被翻译为机器代码,但是:
- 将i的值存储在寄存器A中。
- 将数组地址存储在寄存器B中。
- 添加A和B,结果存储在A中。
- 在这个由a表示的新地址,存储x的值。
- 在寄存器A/中存储i的值效率很低,因为这里有额外的指令,我们已经做过一次。
- 递增寄存器A。
- 将寄存器A存储在i中。
编译器也可以更有效地生成代码,例如:
- 将i的值存储在寄存器A中。
- 将数组地址存储在寄存器B中。
- 添加A和B,结果存储在B中。
- 递增寄存器A。
- 将寄存器A存储在i中。
- …//其余代码。
仅仅因为你作为一个C程序员被训练成认为后缀++发生在末尾,机器代码就不必以这种方式被订购。
因此,在C语言中前缀和后缀++之间没有区别。现在作为C程序员,你应该有所不同,在某些情况下不一致地使用前缀,而在其他情况下不一致地使用后缀,这是没有任何理由的。这表明他们不确定C是如何工作的,或者他们对语言的了解不正确。这始终是一个坏迹象,反过来也表明他们在他们的计划中,基于迷信或"宗教教条"做出了其他有问题的决定。
"前缀++总是更快"确实是一个错误的教条,在未来的C程序员中很常见。
- 没有人说"前缀+总是更快"。那是错误的引用。他们说的是"后缀+永远不会更快"。
- @Pacerier我不是在引用任何特定的人,而是一个广泛传播的、错误的信仰。
- @ BaleKeStar C++!= C
- @ BalkeStar的问答都讲C++。它不同于C,因为它有运算符重载和复制构造函数等。
- 啊,对不起。误读了。是的,对于原始类型,没有性能增益。将删除我的评论。
- @ BaleKeStar C++=C & & & +++C!= C
型
请不要让"哪一个更快"的问题成为决定使用哪一个的因素。很可能你永远不会那么在意,而且,程序员阅读时间比机器时间要贵得多。
使用对人类来说最有意义的代码。
- 我认为,相对于实际效率的提高和整体意图的清晰性,更倾向于模糊易读性的改进是错误的。
- 我的术语"程序员阅读时间"大致类似于"清晰的意图"。实际的效率提高"往往是无法估量的,接近于零就可以称之为零。在OP的例子中,除非对代码进行了分析,以发现+I是一个瓶颈,否则哪个代码更快的问题是浪费时间和程序员的思维单元。
- ++I和I++的可读性差异只是个人偏好的问题,但我清楚地暗示了比I++更简单的操作,尽管在涉及优化编译器时,结果相当于小情况和简单数据类型。因此,当不需要后增量的特定属性时,++i是我的赢家。
- 你在说我说的话。显示意图和提高可读性比担心"效率"更重要。
- 我还是不同意。如果可读性在优先级列表中的位置更高,那么您对编程语言的选择可能是错误的。C/C++的主要目的是编写高效的代码。
型
首先:i++和++i之间的区别在c中是可以忽略的。
到细节。
1.众所周知的C++问题:EDCOX1〔1〕更快。
在C++中,EDOCX1×1是更有效的IFF EDCOX1,4是一种具有过载增量运算符的对象。
为什么?在++i中,对象首先递增,然后可以作为常量引用传递给任何其他函数。如果表达式是foo(i++),这是不可能的,因为现在需要在调用foo()之前完成增量,但旧值需要传递给foo()。因此,编译器在执行原始的递增运算符之前,必须复制i。额外的构造函数/析构函数调用是不好的部分。
如上所述,这不适用于基本类型。
2.第二步。鲜为人知的事实是:i++可能更快
如果不需要调用构造函数/析构函数(在C中总是如此),那么++i和i++应该同样快,对吗?不,他们的速度几乎一样快,但可能存在一些小的差异,大多数其他回答者都是错误的。
i++如何更快?关键是数据依赖性。如果需要从内存中加载该值,则需要对其执行两个后续操作,分别递增和使用该值。使用++i时,需要先进行递增,然后才能使用该值。使用i++时,使用不依赖于增量,CPU可以与增量操作并行执行使用操作。这种差异至多只有一个CPU周期,所以它确实是可忽略的,但它确实存在。这与许多人所期望的相反。
- 关于第2点:如果在另一个表达式中使用了++i或i++,那么在它们之间进行更改会改变表达式的语义,因此任何可能的性能增益/损耗都是不可能的。如果它们是独立的,即不立即使用操作结果,那么任何合适的编译器都会将其编译为相同的东西,例如INC汇编指令。
- @沙巴兹,这是完全正确的,但不是重点。1)尽管语义不同,但在几乎所有可能的情况下,通过逐个调整循环常量,i++和++i都可以互换使用,因此它们在为程序员所做的工作中几乎是等效的。2)尽管两个编译到同一条指令,但它们的执行对于CPU是不同的。在i++的情况下,CPU可以与其他使用相同值的指令并行计算增量(CPU确实这样做!),而对于++i,CPU必须在增量之后调度另一条指令。
- @以沙巴兹为例:if(++foo == 7) bar();和if(foo++ == 6) bar();在功能上是等效的。但是,第二个周期可能更快,因为比较和增量可以由CPU并行计算。不是说这个单一的循环很重要,而是有区别。
- 好点。常量显示了很多(例如<与<=之间),其中通常使用++,因此两者之间的转换通常很容易实现。
- 我喜欢第2点,但这只适用于使用值的情况,righ?问题是"如果不使用结果值?",所以可能会令人困惑。
型
@标记即使编译器可以优化掉变量的临时副本(基于堆栈),而gcc(在最新版本中)正在这样做,并不意味着所有编译器都会这样做。
我刚刚用我们在当前项目中使用的编译器测试了它,4个编译器中有3个没有优化它。
永远不要假设编译器能够正确地完成任务,特别是如果代码可能更快,但速度永远不会变慢,那么就很容易阅读。
如果代码中没有一个真正愚蠢的运算符实现:
Alwas更喜欢++I而不是I++。
- 只是好奇…为什么在一个项目中使用4个不同的C编译器?或者在一个团队或一个公司,就这点而言?
- 当为控制台创建游戏时,每个平台都有自己的编译器/工具链。在一个完美的世界里,我们可以将gcc/clang/llvm用于所有目标,但在这个世界里,我们必须忍受微软、英特尔、MetroWorks、索尼等公司。
在C语言中,如果结果未被使用,编译器通常可以将它们优化为相同的。
但是,在C++中,如果使用提供自己的++操作符的其他类型,前缀版本可能会比后缀版本快。因此,如果不需要后缀语义,最好使用前缀操作符。
型
我可以想到一种情况,即后缀比前缀增量慢:
假设一个寄存器为A的处理器被用作累加器,它是许多指令中使用的唯一寄存器(一些小型微控制器实际上是这样)。
现在想象一下下面的程序及其翻译成一个假设的程序集:
前缀增量:
1 2 3 4 5 6 7 8 9 10 11 12
| a = ++b + c;
; increment b
LD A, [&b]
INC A
ST A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a] |
后缀增量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| a = b++ + c;
; load b
LD A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
; increment b
LD A, [&b]
INC A
ST A, [&b] |
号
注意如何强制重新加载EDOCX1[2]的值。使用前缀增量,编译器可以增加值并继续使用它,可能避免重新加载它,因为所需的值已经在增量之后的寄存器中。但是,对于后缀增量,编译器必须处理两个值,一个是旧值,另一个是增量值,正如我上面所示,这将导致更多的内存访问。
当然,如果不使用增量值,例如单个i++;语句,编译器可以(并且确实)生成一条增量指令,而不管后缀或前缀的用法如何。
作为旁注,我想说的是,有一个b++的表达式不能简单地转换为有++b的表达式,而不需要任何额外的努力(例如,通过添加- 1)。因此,如果这两个表达式是某个表达式的一部分,则对它们进行比较并不真正有效。通常,在表达式中使用b++时,不能使用++b,因此即使++b可能更有效,它也只是错误的。当然,如果表达式需要例外(例如,a = b++ + 1;可以更改为a = ++b;)。
型
不过,我总是喜欢预增量…
我想指出的是,即使在调用operator++函数的情况下,如果函数是内联的,编译器也能够优化临时函数。由于operator++通常很短,并且常常在头中实现,因此它很可能是内联的。
因此,出于实际目的,这两种形式的性能可能没有太大差别。但是,我总是更喜欢预增量,因为它似乎更好地直接表达我想说的内容,而不是依赖于优化器来解决。
另外,给optmizer更少的操作可能意味着编译器运行得更快。
- 您的帖子是C++特定的,而问题是关于C。在任何情况下,您的答案都是错误的:对于自定义POST增量运算符,编译器通常不能产生高效的代码。
- 他说"如果函数是内联的",这使得他的推理是正确的。
- 由于c不提供运算符重载,因此pre和post问题在很大程度上是无趣的。编译器可以使用应用于任何其他未使用的原始计算值的相同逻辑来优化未使用的temp。请参阅所选答案以获取示例。
我的C有点生锈,所以我提前道歉。很快,我就能理解结果。但是,对于这两个文件是如何输出到同一个MD5哈希的,我感到困惑。也许for循环运行相同,但以下两行代码不会生成不同的程序集吗?
VS
第一个将值写入数组,然后递增i。第二个将值写入数组。我不是汇编专家,但我不知道这两行代码如何生成相同的可执行文件。
就我的两分钱。
- @jason z编译器优化发生在程序集生成完成之前,它将看到i变量不在同一行的任何其他地方使用,因此保存它的值是浪费,它可能会有效地将其翻转到i++。但这只是一个猜测。我不能等到我的一个讲师试图说它更快,我会成为一个用实际证据纠正理论的人。我几乎已经感觉到敌意了^^
- "我的C有点生锈,所以我提前道歉。"你的C没什么问题,但是你没有完整地阅读原始问题:"如果不使用结果值,我++和我的性能有区别吗?"注意斜体。在您的示例中,使用了i++/++i的"result",但在惯用的for循环中,不使用前/后递增运算符的"result",因此编译器可以执行它喜欢的操作。
- 在您的示例中,代码是不同的,因为您使用的是值。它们相同的例子是只使用+作为增量,而不使用任何一个返回的值。
- 修复:"编译器会发现没有使用i++的返回值,因此它会将其翻转为++i"。你所写的也是错误的,因为你不能把我和一个i++,i++放在同一行(语句),结果是未定义的。
- 将foo[i++]更改为foo[++i],而不更改任何其他内容,显然会改变程序语义,但在某些处理器上,当使用不带循环提升优化逻辑的编译器时,增加p和q一次,然后运行执行(如*(p++)=*(q++);)的循环比使用执行(如*(p++)=*(q++);)的循环更快。执行*(++pp)=*(++q);。对于某些处理器上的非常紧密的循环,速度差异可能非常显著(超过10%),但这可能是C中唯一的情况,在C中,后增量实质上比前增量快。