ChatGPT解决这个技术问题 Extra ChatGPT

C 中的 i++ 和 ++i 之间是否存在性能差异?

如果不使用结果值,i++++i 之间是否存在性能差异?

相关的,相同的问题,但对于 C++ 明确的是 stackoverflow.com/questions/24901/…

Z
Ziezi

执行摘要:没有。

i++ 可能比 ++i 慢,因为可能需要保存 i 的旧值以供以后使用,但实际上所有现代编译器都会优化它。

我们可以通过查看此函数的代码(包括 ++ii++)来证明这一点。

$ cat i++.c
extern void g(int i);
void f()
{
    int i;

    for (i = 0; i < 100; i++)
        g(i);

}

除了 ++ii++ 之外,这些文件是相同的:

$ 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

我知道这个问题是关于 C 的,但我很想知道浏览器是否可以对 javascript 进行这种优化。
因此,对于您测试的一个编译器,“否”是正确的。
@Andreas:好问题。我曾经是一名编译器编写者,有机会在许多 CPU、操作系统和编译器上测试这段代码。我发现唯一没有优化 i++ 案例的编译器(事实上,专业地引起我注意的编译器)是 Walt Bilofsky 的 Software Toolworks C80 编译器。该编译器适用于 Intel 8080 CP/M 系统。可以肯定地说,任何不包含此优化的编译器都不适合一般用途。
尽管性能差异可以忽略不计,并且在许多情况下进行了优化 - 请注意,使用 ++i 而不是 i++ 仍然是一种好习惯。绝对没有理由不这样做,如果您的软件曾经通过一个没有优化它的工具链,那么您的软件将会更有效率。考虑到输入 ++i 和输入 i++ 一样容易,所以一开始就没有理由不使用 ++i
@monokrome 由于开发人员可以为多种语言的前缀和后缀运算符提供自己的实现,因此对于编译器而言,在不首先比较这些函数的情况下,这可能并不总是一个可接受的解决方案,这可能很重要。
Z
Ziezi

来自 Andrew Koenig 的 Efficiency versus intent

首先,++i 比 i++ 更有效这一点远非显而易见,至少在涉及整数变量的情况下是这样。

和 :

因此,应该问的问题不是这两个操作中的哪一个更快,而是这两个操作中的哪一个更准确地表达了您要完成的任务。我认为,如果您不使用表达式的值,则永远没有理由使用 i++ 而不是 ++i,因为永远没有理由复制变量的值,递增变量,然后抛出复制走。

因此,如果不使用结果值,我会使用 ++i。但不是因为它更有效:因为它正确地说明了我的意图。


我们不要忘记其他一元运算符也是前缀。我认为 ++i 是使用一元运算符的“语义”方式,而 i++ 是为了满足特定需求(添加前评估)。
如果不使用结果值,则在语义上没有区别:即,没有选择一个或另一个构造的基础。
作为读者,我看到了不同。所以作为一名作家,我会通过选择一个而不是另一个来表达我的意图。这只是我的一个习惯,尝试通过代码与我的程序员朋友和队友交流:)
按照 Koenig 的建议,我编写 i++ 的方式与编写 i += ni = i + n 的方式相同,即采用 target verb object< 的形式/i>,target 操作数位于 verb 运算符的左侧。在 i++ 的情况下,没有右 object,但规则仍然适用,将 target 保持在 verb 的左侧操作员。
如果你试图增加 i,那么 i++ 和 ++i 都正确地表达了你的意图。没有理由偏爱其中之一。作为读者,我认为没有区别,您的同事在看到 i++ 时是否会感到困惑并认为,也许这是一个错字,他不是故意增加 i 吗?
M
Markus Safar

更好的答案是 ++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++ 的习惯仍然是一种好习惯,这样结果不会受到影响。


该问题明确指出 C,没有提及 C++。
这个(诚然老的)问题是关于 C,而不是 C++,但我认为还值得一提的是,在 C++ 中,即使一个类实现了后缀和前缀运算符,它们甚至不一定相关。例如 bar++ 可能会增加一个数据成员,而 ++bar 可能会增加一个不同的数据成员,在这种情况下,您将无法选择使用其中任何一个,因为语义不同。
-1 虽然这对 C++ 程序员来说是个好建议,但它丝毫没有回答标记为 C 的问题。在 C 中,使用前缀还是后缀绝对没有区别。
@Pacerier问题被标记为C,并且仅标记为C。你为什么认为他们对此不感兴趣?鉴于 SO 的工作原理,假设他们只对 C 而不是 Java、C#、COBOL 或任何其他离题语言感兴趣,难道不是很聪明吗?
L
Lundin

简短的回答:

i++++i 在速度方面从来没有任何区别。一个好的编译器不应该在这两种情况下生成不同的代码。

长答案:

其他所有答案都没有提到的是,++ii++ 之间的区别仅在找到的表达式中才有意义。

for(i=0; i<n; i++) 的情况下,i++ 在它自己的表达式中是单独的:在 i++ 之前有一个序列点,在它之后有一个。因此,生成的唯一机器代码是“将 i 增加 1”,并且明确定义了与程序其余部分相关的顺序。因此,如果您将其更改为前缀 ++,那一点也不重要,您仍然会得到机器代码“将 i 增加 1”。

++ii++ 之间的区别仅在诸如 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 程序员中常见的错误教条之一。


没有人说“前缀 ++ 总是更快”。那是错误的引用。他们说的是“Postfix ++ 永远不会更快”。
@Pacerier我没有引用任何特定的人,而只是一个广泛传播的不正确的信念。
@rbaleksandar c++ == c && ++c != c
J
JProgrammer

借鉴 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 指出的那样,实际上没有区别,因为可以进行编译器优化。


我认为他的问题是针对 C 的,但对于 C++,你是绝对正确的,而且 C 人应该采用这一点,因为他们也可以将它用于 C++。我经常看到 C 程序员使用后缀语法 ;-)
-1 虽然这对 C++ 程序员来说是个好建议,但它丝毫没有回答标记为 C 的问题。在 C 中,使用前缀还是后缀绝对没有区别。
根据 Andreas 的答案,@Lundin 前缀和 postifx 在 C 中确实很重要。你不能假设编译器会优化掉,对于任何语言来说,总是喜欢 ++i 而不是 i++ 是一种很好的做法
@JProgrammer 根据您的真诚回答,它们并不重要:) 如果您发现它们确实产生了不同的代码,那要么是因为您未能启用优化,要么是因为编译器不好。无论如何,您的答案是题外话,因为问题是关于 C 的。
Z
Ziezi

如果您担心微优化,这里有一个额外的观察。递减循环“可能”比递增循环更有效(取决于指令集架构,例如 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 系统开发人员指南中记住。


好一个。但这不会减少缓存命中吗?
代码优化书籍中有一个古老的奇怪技巧,将零测试分支的优势与增加的地址相结合。这是高级语言的示例(可能没用,因为许多编译器足够聪明,可以用效率较低但更常见的循环代码替换它): int a[N]; for( i = -N; i ; ++i) a[N+i] += 123;
@noop 您能否详细说明您参考了哪些代码优化书籍?我一直在努力寻找好的。
这个答案根本不是问题的答案。
@mezamorphic 对于 x86,我的资料来源是:英特尔优化手册、Agner Fog、Paul Hsieh、Michael Abrash。
C
Community

首先:i++++i 之间的区别在 C 中可以忽略不计。

到细节。

1. 众所周知的 C++ 问题:++i 更快

在 C++ 中,如果 i 是某种具有重载增量运算符的对象,则 ++i 更有效。

为什么?
++i 中,对象首先递增,然后可以作为 const 引用传递给任何其他函数。如果表达式为 foo(i++),则这是不可能的,因为现在需要在调用 foo() 之前完成增量,但需要将旧值传递给 foo()。因此,编译器在对原始文件执行增量运算符之前被迫制作 i 的副本。额外的构造函数/析构函数调用是不好的部分。

如上所述,这不适用于基本类型。

2. 鲜为人知的事实:i++ 可能更快

如果不需要调用构造函数/析构函数,在 C 中总是如此,++ii++ 应该同样快,对吧?不。它们几乎同样快,但可能存在细微差别,大多数其他回答者都搞错了。

i++ 怎样才能更快?
关键是数据依赖性。如果需要从内存中加载该值,则需要对其进行两个后续操作,将其递增并使用它。对于 ++i,增量需要在之前使用该值。使用 i++,使用不依赖于增量,CPU 可以与增量操作并行执行使用操作。差别最多只有一个 CPU 周期,所以真的可以忽略不计,但确实存在。这与许多人所期望的相反。


关于您的第 2 点:如果在另一个表达式中使用 ++ii++,则在它们之间进行更改会改变表达式的语义,因此任何可能的性能增益/损失都是毫无疑问的。如果它们是独立的,即不立即使用操作的结果,那么任何体面的编译器都会将其编译为相同的东西,例如 INC 汇编指令。
@Shahbaz 完全正确,但不是重点。 1) 即使语义不同,i++++i 在几乎所有可能的情况下都可以通过将循环常量调整 1 来互换使用,因此它们在为程序员所做的事情上几乎是等价的。 2)即使两者都编译为相同的指令,但它们的执行对于 CPU 是不同的。在 i++ 的情况下,CPU 可以并行计算增量到使用相同值的其他指令(CPU 确实这样做!),而对于 ++i,CPU 必须调度增量之后的另一条指令。
@Shahbaz 例如:if(++foo == 7) bar();if(foo++ == 6) bar(); 在功能上是等效的。但是,第二个可能快一个周期,因为比较和增量可以由 CPU 并行计算。并不是说这个单一的周期很重要,但区别就在那里。
好点子。通常使用 ++ 的地方经常出现常量(以及 <,例如与 <=),因此通常可以轻松地进行转换。
我喜欢第 2 点,但这仅适用于使用该值的情况,对吗?问题是“如果不使用结果值?”,所以它可能会令人困惑。
A
Andy Lester

请不要让“哪个更快”的问题成为使用哪个的决定因素。您可能永远不会那么在意,此外,程序员阅读时间比机器时间昂贵得多。

使用对阅读代码的人最有意义的那个。


我认为宁愿模糊的可读性改进而不是实际的效率提升和整体意图清晰是错误的。
我的术语“程序员阅读时间”大致类似于“意图清晰”。 “实际效率增益”通常是无法衡量的,接近于零,可以称其为零。在 OP 的情况下,除非已对代码进行分析以发现 ++i 是一个瓶颈,否则哪个更快的问题是浪费时间和程序员的思维单元。
++i 和 i++ 之间的可读性差异只是个人喜好问题,但 ++i 显然意味着比 i++ 更简单的操作,尽管在涉及优化编译器时结果对于琐碎的情况和简单的数据类型是等效的。因此,当不需要后增量的特定属性时,++i 对我来说是赢家。
你在说我在说什么。显示意图和提高可读性比担心“效率”更重要。
我还是不能同意。如果可读性在您的优先级列表中更高,那么您选择的编程语言可能是错误的。 C/C++ 的主要目的是编写高效的代码。
A
Andreas

@Mark即使允许编译器优化变量的(基于堆栈的)临时副本并且gcc(在最新版本中)正在这样做,但这并不意味着所有编译器都会这样做。

我只是用我们在当前项目中使用的编译器对其进行了测试,四分之三的编译器没有对其进行优化。

永远不要假设编译器做对了,特别是如果可能更快但绝不慢的代码很容易阅读。

如果您的代码中没有一个非常愚蠢的运算符实现:

一直喜欢 ++i 而不是 i++。


只是好奇...为什么在一个项目中使用 4 种不同的 C 编译器?或者在一个团队或一个公司中,就此而言?
在为控制台创建游戏时,每个平台都会带来自己的编译器/工具链。在一个完美的世界中,我们可以对所有目标使用 gcc/clang/llvm,但在这个世界中,我们必须忍受 Microsoft、Intel、Metroworks、Sony 等。
K
Kristopher Johnson

在 C 语言中,如果结果未使用,编译器通常可以将它们优化为相同。

但是,在 C++ 中,如果使用提供自己的 ++ 运算符的其他类型,则前缀版本可能会比后缀版本快。因此,如果您不需要后缀语义,最好使用前缀运算符。


d
daShier

我一直在阅读这里的大部分答案和许多评论,但我没有看到任何对 one 实例的引用,我认为 i++ 比 {2 更有效}(也许令人惊讶的是,--i i-- 更高效)。这适用于 DEC PDP-11 的 C 编译器!

PDP-11 具有寄存器预减和后增的汇编指令,但反之则不然。这些指令允许将任何“通用”寄存器用作堆栈指针。因此,如果您使用 *(i++) 之类的东西,它可以编译成一条汇编指令,而 *(++i) 则不能。

这显然是一个非常深奥的例子,但它确实提供了后增量更有效的例外(或者我应该说是,因为这些天对 PDP-11 C 代码的需求不多)。


@daShier。 +1,尽管我不同意,但这并不是那么深奥,或者至少不应该如此。 70 年代初,当 PDP-11 是目标处理器时,C 与 Unix 在 AT&T 贝尔实验室共同开发。在这个时代的 Unix 源代码中,后增量“i++”更为普遍,部分原因是开发人员知道何时分配值“j = i++”或用作索引,“a[i++] = n”,代码会稍微快一些(而且更小)。除非需要 pre ,否则他们似乎养成了使用 post increment 的习惯。其他人通过阅读他们的代码来学习,也养成了这个习惯。
68000 具有相同的功能,硬件支持后递增和预递减(作为寻址模式),但反之则不行。摩托罗拉团队受到 DEC PDP-11 的启发。
@jimhark,是的,我是那些过渡到 68000 并且仍然使用 --ii++ 的 PDP-11 程序员之一。
S
Shahbaz

我可以想到后缀比前缀增量慢的情况:

想象一个带有寄存器 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++ 特定的,而问题是关于 C 的。无论如何,您的答案都是错误的:对于自定义后增量运算符,编译器通常无法生成高效代码。
他说“如果函数被内联”,这使得他的推理是正确的。
由于 C 不提供运算符重载,因此 pre 与 post 的问题在很大程度上是无趣的。编译器可以使用应用于任何其他未使用的原始计算值的相同逻辑来优化未使用的临时值。请参阅所选答案以获取示例。
J
Jason Z

我的C有点生锈,所以我提前道歉。 Speedwise,我可以理解结果。但是,我对这两个文件如何输出相同的 MD5 哈希感到困惑。也许 for 循环运行相同,但以下 2 行代码不会生成不同的程序集吗?

myArray[i++] = "hello";

对比

myArray[++i] = "hello";

第一个将值写入数组,然后递增 i。然后第二个增量 i 写入数组。我不是汇编专家,但我只是看不到这两条不同的代码行将如何生成相同的可执行文件。

只是我的两分钱。


@Jason Z编译器优化发生在程序集生成完成之前,它会看到 i 变量在同一行的其他任何地方都没有使用,因此保留它的值将是一种浪费,它可能有效地将其翻转为 i++。但这只是一个猜测。我等不及我的一位讲师试图说它更快,并且我要成为用实际证据纠正理论的人。我几乎已经可以感受到敌意了^_^
“我的C有点生锈,所以我提前道歉。”您的 C 没有问题,但您没有完整阅读原始问题:“如果不使用结果值,i++ 和 ++i 之间是否存在性能差异?”注意斜体。在您的示例中,使用了 i++/++i 的“结果”,但在惯用的 for 循环中,未使用前/后增量运算符的“结果”,因此编译器可以做它喜欢的事情。
在您的示例中,代码会有所不同,因为您正在使用该值。它们相同的示例仅使用 ++ 作为增量,而不使用两者返回的值。
修复:“编译器会看到 i++ 的返回值未使用,因此会将其翻转为 ++i”。您写的内容也是错误的,因为您不能将 i 与 i++、i++ 之一放在同一行(语句)上,结果未定义。
在不更改任何其他内容的情况下将 foo[i++] 更改为 foo[++i] 显然会改变程序语义,但在某些处理器上,当使用没有循环提升优化逻辑的编译器时,将 pq 递增一次,然后运行执行的循环例如,*(p++)=*(q++); 会比使用执行 *(++pp)=*(++q); 的循环更快。对于某些处理器上非常紧凑的循环,速度差异可能很大(超过 10%),但这可能是 C 语言中唯一一种后增量比前增量快得多的情况。