如果不使用结果值,i++
和 ++i
之间是否存在性能差异?
执行摘要:没有。
i++
可能比 ++i
慢,因为可能需要保存 i
的旧值以供以后使用,但实际上所有现代编译器都会优化它。
我们可以通过查看此函数的代码(包括 ++i
和 i++
)来证明这一点。
$ cat i++.c
extern void g(int i);
void f()
{
int i;
for (i = 0; i < 100; i++)
g(i);
}
除了 ++i
和 i++
之外,这些文件是相同的:
$ diff i++.c ++i.c
6c6
< for (i = 0; i < 100; i++)
---
> for (i = 0; i < 100; ++i)
我们将编译它们,并获得生成的汇编器:
$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c
我们可以看到生成的目标文件和汇编文件都是一样的。
$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e
$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
来自 Andrew Koenig 的 Efficiency versus intent:
首先,++i 比 i++ 更有效这一点远非显而易见,至少在涉及整数变量的情况下是这样。
和 :
因此,应该问的问题不是这两个操作中的哪一个更快,而是这两个操作中的哪一个更准确地表达了您要完成的任务。我认为,如果您不使用表达式的值,则永远没有理由使用 i++ 而不是 ++i,因为永远没有理由复制变量的值,递增变量,然后抛出复制走。
因此,如果不使用结果值,我会使用 ++i
。但不是因为它更有效:因为它正确地说明了我的意图。
i++
的方式与编写 i += n
或 i = i + n
的方式相同,即采用 target verb object< 的形式/i>,target 操作数位于 verb 运算符的左侧。在 i++
的情况下,没有右 object,但规则仍然适用,将 target 保持在 verb 的左侧操作员。
更好的答案是 ++i
有时会更快,但绝不会更慢。
每个人似乎都认为 i
是常规的内置类型,例如 int
。在这种情况下,不会有可测量的差异。
但是,如果 i
是复杂类型,那么您很可能会发现可测量的差异。对于 i++
,您必须在递增之前复制您的类。根据副本中涉及的内容,它确实可能会更慢,因为使用 ++i
您可以只返回最终值。
Foo Foo::operator++()
{
Foo oldFoo = *this; // copy existing value - could be slow
// yadda yadda, do increment
return oldFoo;
}
另一个区别是使用 ++i
您可以选择返回引用而不是值。同样,根据制作对象副本所涉及的内容,这可能会更慢。
可能发生这种情况的真实示例是使用迭代器。复制迭代器不太可能成为应用程序的瓶颈,但养成使用 ++i
而不是 i++
的习惯仍然是一种好习惯,这样结果不会受到影响。
简短的回答:
i++
和 ++i
在速度方面从来没有任何区别。一个好的编译器不应该在这两种情况下生成不同的代码。
长答案:
其他所有答案都没有提到的是,++i
与 i++
之间的区别仅在找到的表达式中才有意义。
在 for(i=0; i<n; i++)
的情况下,i++
在它自己的表达式中是单独的:在 i++
之前有一个序列点,在它之后有一个。因此,生成的唯一机器代码是“将 i
增加 1
”,并且明确定义了与程序其余部分相关的顺序。因此,如果您将其更改为前缀 ++
,那一点也不重要,您仍然会得到机器代码“将 i
增加 1
”。
++i
和 i++
之间的区别仅在诸如 array[i++] = x;
与 array[++i] = x;
之类的表达式中很重要。有些人可能会争辩说后缀在此类操作中会变慢,因为 i
所在的寄存器必须稍后重新加载。但是请注意,编译器可以随意以任何方式对您的指令进行排序,只要它不会像 C 标准所称的那样“破坏抽象机器的行为”。
因此,虽然您可能假设 array[i++] = x;
被翻译为机器代码:
将 i 的值存储在寄存器 A 中。
将数组的地址存储在寄存器 B 中。
添加A和B,将结果存储在A中。
在这个由 A 表示的新地址处,存储 x 的值。
将 i 的值存储在寄存器 A // 效率低,因为这里有额外的指令,我们已经这样做了一次。
递增寄存器 A。
将寄存器 A 存储在 i 中。
编译器还不如更有效地生成代码,例如:
将 i 的值存储在寄存器 A 中。
将数组的地址存储在寄存器 B 中。
添加A和B,将结果存储在B中。
递增寄存器 A。
将寄存器 A 存储在 i 中。
... // 其余代码。
仅仅因为作为 C 程序员的您被训练认为后缀 ++
出现在末尾,机器代码不必以这种方式排序。
因此,C 中的前缀和后缀 ++
之间没有区别。现在,作为 C 程序员,您应该有所不同的是,人们在某些情况下不一致地使用前缀而在其他情况下使用后缀,没有任何理由。这表明他们不确定 C 是如何工作的,或者他们对该语言的了解不正确。这总是一个不好的迹象,它反过来表明他们正在他们的计划中做出其他有问题的决定,基于迷信或“宗教教条”。
“前缀 ++
总是更快”确实是 C 程序员中常见的错误教条之一。
借鉴 Scott Meyers,More Effective c++ 第 6 项:区分递增和递减运算的前缀和后缀形式。
就对象而言,前缀版本总是优于后缀版本,尤其是在迭代器方面。
如果您查看运营商的调用模式,就会出现这种情况的原因。
// Prefix
Integer& Integer::operator++()
{
*this += 1;
return *this;
}
// Postfix
const Integer Integer::operator++(int)
{
Integer oldValue = *this;
++(*this);
return oldValue;
}
看这个例子很容易看出前缀运算符总是比后缀更有效。因为在使用后缀时需要临时对象。
这就是为什么当您看到使用迭代器的示例时,它们总是使用前缀版本。
但是正如您为 int 指出的那样,实际上没有区别,因为可以进行编译器优化。
如果您担心微优化,这里有一个额外的观察。递减循环“可能”比递增循环更有效(取决于指令集架构,例如 ARM),给定:
for (i = 0; i < 100; i++)
在每个循环中,您将分别获得一条指令:
i 加 1。比较 i 是否小于 100。如果 i 小于 100,则进行条件分支。
而递减循环:
for (i = 100; i != 0; i--)
该循环将为以下各项提供一条指令:
递减 i,设置 CPU 寄存器状态标志。取决于 CPU 寄存器状态 (Z==0) 的条件分支。
当然,这仅在减为零时才有效!
从 ARM 系统开发人员指南中记住。
首先:i++
和 ++i
之间的区别在 C 中可以忽略不计。
到细节。
1. 众所周知的 C++ 问题:++i 更快
在 C++ 中,如果 i
是某种具有重载增量运算符的对象,则 ++i
更有效。
为什么?
在 ++i
中,对象首先递增,然后可以作为 const 引用传递给任何其他函数。如果表达式为 foo(i++)
,则这是不可能的,因为现在需要在调用 foo()
之前完成增量,但需要将旧值传递给 foo()
。因此,编译器在对原始文件执行增量运算符之前被迫制作 i
的副本。额外的构造函数/析构函数调用是不好的部分。
如上所述,这不适用于基本类型。
2. 鲜为人知的事实:i++ 可能更快
如果不需要调用构造函数/析构函数,在 C 中总是如此,++i
和 i++
应该同样快,对吧?不。它们几乎同样快,但可能存在细微差别,大多数其他回答者都搞错了。
i++
怎样才能更快?
关键是数据依赖性。如果需要从内存中加载该值,则需要对其进行两个后续操作,将其递增并使用它。对于 ++i
,增量需要在之前使用该值。使用 i++
,使用不依赖于增量,CPU 可以与增量操作并行执行使用操作。差别最多只有一个 CPU 周期,所以真的可以忽略不计,但确实存在。这与许多人所期望的相反。
++i
或 i++
,则在它们之间进行更改会改变表达式的语义,因此任何可能的性能增益/损失都是毫无疑问的。如果它们是独立的,即不立即使用操作的结果,那么任何体面的编译器都会将其编译为相同的东西,例如 INC
汇编指令。
i++
和 ++i
在几乎所有可能的情况下都可以通过将循环常量调整 1 来互换使用,因此它们在为程序员所做的事情上几乎是等价的。 2)即使两者都编译为相同的指令,但它们的执行对于 CPU 是不同的。在 i++
的情况下,CPU 可以并行计算增量到使用相同值的其他指令(CPU 确实这样做!),而对于 ++i
,CPU 必须调度在增量之后的另一条指令。
if(++foo == 7) bar();
和 if(foo++ == 6) bar();
在功能上是等效的。但是,第二个可能快一个周期,因为比较和增量可以由 CPU 并行计算。并不是说这个单一的周期很重要,但区别就在那里。
++
的地方经常出现常量(以及 <
,例如与 <=
),因此通常可以轻松地进行转换。
请不要让“哪个更快”的问题成为使用哪个的决定因素。您可能永远不会那么在意,此外,程序员阅读时间比机器时间昂贵得多。
使用对阅读代码的人最有意义的那个。
@Mark即使允许编译器优化变量的(基于堆栈的)临时副本并且gcc(在最新版本中)正在这样做,但这并不意味着所有编译器都会这样做。
我只是用我们在当前项目中使用的编译器对其进行了测试,四分之三的编译器没有对其进行优化。
永远不要假设编译器做对了,特别是如果可能更快但绝不慢的代码很容易阅读。
如果您的代码中没有一个非常愚蠢的运算符实现:
一直喜欢 ++i 而不是 i++。
在 C 语言中,如果结果未使用,编译器通常可以将它们优化为相同。
但是,在 C++ 中,如果使用提供自己的 ++ 运算符的其他类型,则前缀版本可能会比后缀版本快。因此,如果您不需要后缀语义,最好使用前缀运算符。
我一直在阅读这里的大部分答案和许多评论,但我没有看到任何对 one 实例的引用,我认为 i++
比 {2 更有效}(也许令人惊讶的是,--i
比 i--
更高效)。这适用于 DEC PDP-11 的 C 编译器!
PDP-11 具有寄存器预减和后增的汇编指令,但反之则不然。这些指令允许将任何“通用”寄存器用作堆栈指针。因此,如果您使用 *(i++)
之类的东西,它可以编译成一条汇编指令,而 *(++i)
则不能。
这显然是一个非常深奥的例子,但它确实提供了后增量更有效的例外(或者我应该说是,因为这些天对 PDP-11 C 代码的需求不多)。
--i
和 i++
的 PDP-11 程序员之一。
我可以想到后缀比前缀增量慢的情况:
想象一个带有寄存器 A
的处理器被用作累加器,它是许多指令中唯一使用的寄存器(一些小型微控制器实际上是这样的)。
现在想象以下程序及其翻译成一个假设的程序集:
前缀增量:
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]
后缀增量:
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]
请注意如何强制重新加载 b
的值。使用前缀增量,编译器可以只增加值并继续使用它,可能避免重新加载它,因为增量后所需的值已经在寄存器中。但是,对于后缀增量,编译器必须处理两个值,一个是旧值,一个是增量值,正如我在上面显示的那样,会导致更多的内存访问。
当然,如果不使用增量值,例如单个 i++;
语句,编译器可以(并且确实)简单地生成增量指令,而不管后缀或前缀的使用。
作为旁注,我想提一下,如果没有任何额外的努力(例如通过添加 - 1
),其中存在 b++
的表达式不能简单地转换为带有 ++b
的表达式。因此,如果它们是某些表达式的一部分,则比较两者并不是真正有效的。通常,在表达式中使用 b++
时,您不能使用 ++b
,因此即使 ++b
可能更有效,它也只是错误的。当然,如果表达式请求它,则例外(例如,可以将 a = b++ + 1;
更改为 a = ++b;
)。
我总是更喜欢预增量,但是......
我想指出,即使在调用 operator++ 函数的情况下,如果函数被内联,编译器也能够优化掉临时变量。由于 operator++ 通常很短并且经常在标头中实现,因此它很可能被内联。
因此,出于实际目的,这两种形式的性能可能没有太大区别。但是,我总是更喜欢预增量,因为直接表达我想说的话似乎更好,而不是依靠优化器来解决它。
此外,减少优化器的工作量可能意味着编译器运行得更快。
我的C有点生锈,所以我提前道歉。 Speedwise,我可以理解结果。但是,我对这两个文件如何输出相同的 MD5 哈希感到困惑。也许 for 循环运行相同,但以下 2 行代码不会生成不同的程序集吗?
myArray[i++] = "hello";
对比
myArray[++i] = "hello";
第一个将值写入数组,然后递增 i。然后第二个增量 i 写入数组。我不是汇编专家,但我只是看不到这两条不同的代码行将如何生成相同的可执行文件。
只是我的两分钱。
foo[i++]
更改为 foo[++i]
显然会改变程序语义,但在某些处理器上,当使用没有循环提升优化逻辑的编译器时,将 p
和 q
递增一次,然后运行执行的循环例如,*(p++)=*(q++);
会比使用执行 *(++pp)=*(++q);
的循环更快。对于某些处理器上非常紧凑的循环,速度差异可能很大(超过 10%),但这可能是 C 语言中唯一一种后增量比前增量快得多的情况。
不定期副业成功案例分享
++i
而不是i++
仍然是一种好习惯。绝对没有理由不这样做,如果您的软件曾经通过一个没有优化它的工具链,那么您的软件将会更有效率。考虑到输入++i
和输入i++
一样容易,所以一开始就没有理由不使用++i
。