避免使用Postfix增量运算符


25

我已经读到由于性能原因(在某些情况下),我应该避免使用后缀增量运算符

但这不影响代码的可读性吗?在我看来:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

看起来比:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

但这可能只是出于习惯。诚然,我没有看到很多用处++i

在这种情况下,性能是否牺牲了可读性?还是我只是盲目的,++i可读性比i++


1
i++在不知道会影响性能的情况下使用过++i,所以我切换了。刚开始时,后者看上去确实有些奇怪,但是过了一会儿我就习惯了,现在感觉就像i++
gablin 2011年

15
++i并且i++在某些情况下做不同的事情,不要假设它们是相同的。
2011年

2
这是关于C还是C ++?他们是两个非常不同的语言!:-)在C ++中,惯用的for循环是for (type i = 0; i != 42; ++i)。不仅可以operator++被重载,但是这样能operator!=operator<。前缀增量不比后缀贵,不等于不比小于贵。我们应该使用哪个?
Bo Persson

7
它不应该被称为++ C吗?
Armand

21
@Stephen:C ++手段采取C,添加到它,然后使用旧的
超级猫

Answers:


58

事实:

  1. i ++和++ i同样易于阅读。您不喜欢它是因为您不习惯它,但是从本质上讲,您没有什么可以误解的,因此读或写它不再是一件费力的事情。

  2. 至少在某些情况下,后缀运算符的效率会降低。

  3. 但是,在99.99%的情况下,这并不重要,因为(a)无论如何它都将作用于简单或原始类型,并且如果要复制大对象,这只是一个问题(b)不会在性能上出现问题代码(c)的关键部分,您可能不知道编译器是否会对其进行优化。

  4. 因此,我建议您使用前缀,除非您特别需要后缀是一种好习惯,因为(a)与其他事物保持精确一致是一个好习惯,并且(b)在蓝月亮中,您打算使用后缀并以错误的方式解决问题:如果您始终写出您的意思,那就不太可能了。在性能和优化之间始终需要权衡取舍。

您应该使用常识,而不是在需要之前不进行微优化,但是无论如何都不要低效。通常,这意味着:首先,排除即使在非时间紧迫的代码中效率也不可接受的任何代码构造(通常代表基本概念错误的代码构造,例如无缘无故地按值传递500MB对象);其次,在其他所有编写代码的方式中,选择最清晰的方式。

但是,在这里,我相信答案很简单:我相信写前缀,除非您特别需要后缀(a)非常清晰,并且(b)更可能更有效率,所以默认情况下,您应该始终编写前缀,但是如果您忘记了,不必担心。

六个月前,我和您一样认为i ++更自然,但这纯粹是您的习惯。

编辑1:我通常对此事信任的“更有效的C ++”中的斯科特·迈耶斯说,您通常应该避免在用户定义的类型上使用后缀运算符(因为后缀增量功能的唯一明智的实现是使复制对象,调用前缀递增函数执行递增,然后返回副本,但是复制操作可能会很昂贵。

因此,我们不知道关于(a)今天是否正确,(b)它是否也适用于内在类型(c)是否应在其上使用“ ++”是否存在任何通用规则。除了轻量级的迭代器类之外,没有什么比这更重要的了。但是出于上述所有原因,请不要执行我之前所说的事情。

编辑2:这是指一般惯例。如果您认为在某些特定情况下确实很重要,则应该对其进行概要分析并查看。概要分析既简单又便宜,并且可行。从第一原理推论出需要优化的东西是困难且昂贵的,并且是行不通的。


您的帖子正确无误。在infix +运算符和后递增++已重载的表达式中,例如aClassInst = someOtherClassInst + yetAnotherClassInst ++,解析器将在生成执行后递增操作的代码之前生成代码以执行加法运算,从而减轻了对创建一个临时副本。这里的性能杀手不是事后增加。它是使用重载的中缀运算符。中缀运算符产生新实例。
比特币2011年

2
我高度怀疑人们之所以“习惯” i++而不是++i因为此问题/答案中引用的某种流行编程语言的名称……
Shadow

61

始终首先为程序员编码,然后为计算机编码。

如果有性能差异,编译器已经蒙上后的专家的眼睛在你的代码,可以衡量它它的问题-那么你可以改变它。


7
超级声明!!!
戴夫

8
@马丁:这就是为什么我要使用前缀增量。Postfix语义意味着保留旧值,如果不需要它,则使用它是不准确的。
Matthieu M.

1
对于循环索引,这将是更清晰的-但如果你是迭代数组通过增加一个指针和使用前缀意味着起始于开始前的非法地址一个那将是一个提升性能的不错,不论
马丁贝克特

5
@Matthew:根本不正确的是,后增量意味着保留旧值的副本。在一个人查看其输出之前,无法确定编译器如何处理中间值。如果花时间查看带注释的GCC生成的汇编语言列表,您将看到GCC为两个循环生成相同的机器代码。相对于后增量,因为它更有效而偏爱前增量,这只是胡说八道。
比特币2011年

2
@Mathhieu:我发布的代码是在优化关闭的情况下生成的。C ++规范未声明使用后增量时,编译器必须产生值的临时实例。它仅说明了前递增和后递增运算符的优先级。
比特币2011年

13

GCC为两个循环产生相同的机器代码。

C代码

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

汇编代码(我的意见)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

如何启用优化?
serv-inc

2
@user:可能没有变化,但是您实际上希望位捻手很快会回来吗?
Deduplicator

2
注意:虽然在C中没有带有重载运算符的用户定义类型,但在C ++中,从基本类型到用户定义类型的泛化完全是无效的
Deduplicator

@Deduplicator:谢谢您也指出这个答案不能推广到用户定义的类型。在询问之前,我没有看过他的用户页面。
serv-inc

12

97%的时间不用担心性能。过早的优化是万恶之源。

-唐纳德​​·努斯

既然这已经成为我们的障碍,那么让我们明智地做出选择:

  • ++i前缀递增,递增当前值并产生结果
  • i++后缀递增,复制值,递增当前值,产生副本

除非需要复制旧值,否则使用后缀增量是完成任务的一种round回方式。

不准确性源自懒惰,请始终使用最直接的方式来表达您的意图的构造,这比将来的维护者误解您的原始意图的机会要少。

即使在这里(真的)很小,但有时我还是对阅读代码感到困惑:我真的想知道意图和实际表达是否重合,当然,几个月后,它们(或我)也不记得...

因此,它是否对您来说都没关系。拥抱。再过几个月,您将避免使用旧的做法。


4

在C ++中,如果涉及运算符重载,则可能会造成实质性的性能差异,特别是如果您正在编写模板代码并且不知道可能传递什么迭代器。任何迭代器X背后的逻辑可能既实质又重要-也就是说,速度很慢,并且编译器无法对其进行优化。

但这在C语言中不是这种情况,在C语言中,它只会是一个琐碎的类型,并且性能差异是琐碎的,并且编译器可以轻松地进行优化。

提示:您使用C或C ++进行编程,并且问题与一个或另一个相关,而不是两者都相关。


2

任一操作的性能高度依赖于基础架构。必须增加存储在存储器中的值,这意味着在两种情况下冯·诺依曼瓶颈都是限制因素。

对于++ i,我们必须

Fetch i from memory 
Increment i
Store i back to memory
Use i

对于i ++,我们必须

Fetch i from memory
Use i
Increment i
Store i back to memory

++和-运算符将其起源追溯到PDP-11指令集。PDP-11可以对寄存器执行自动后递增。它还可以对寄存器中包含的有效地址执行自动递减。在这两种情况下,只有当所讨论的变量是“寄存器”变量时,编译器才可以利用这些机器级操作。


2

如果您想知道是否有些慢,请进行测试。取一个BigInteger或等效值,使用这两个习惯用法将其放入相似的for循环中,确保循环内部不会被优化,并同时对它们进行计时。

阅读了这篇文章后,我认为它没有说服力,原因有三点。第一,编译器应该能够围绕从未使用过的对象的创建进行优化。第二,该i++概念对于数字for循环而言是惯用的,因此我可以看到实际受影响的情况仅限于。第三,它们提供了纯理论上的论据,没有数字可以支持它。

特别是基于原因1,我的猜测是,当您实际进行计时时,它们将彼此相邻。


-1

首先,它不会影响IMO的可读性。它不是您以前所见的,只是一小会儿您就习惯了。

其次,除非您在代码中使用大量的后缀运算符,否则您可能看不出太大的区别。在可能的情况下不使用它们的主要参数是必须保留原始var值的副本,直到参数末尾仍可以使用原始var为止。根据架构的不同,可以是32位,也可以是64位。等于4或8个字节或0.00390625或0.0078125 MB。除非您要使用大量的此类文件(需要保存很长的时间),否则使用它们的机会非常高,以今天的计算机资源和速度,您甚至不会注意到从后缀转换为前缀的区别。

编辑:忘记这剩余的部分,因为我的结论被证明是错误的(除了++ i和i ++的部分并不总是做相同的事情……这仍然是正确的)。

之前也有人指出,他们在某些情况下不会做同样的事情。如果决定,请小心进行切换。我从来没有尝试过(我一直使用后缀),所以我不确定,但是我认为从后缀更改为前缀将导致不同的结果:(同样,我可能是错的...取决于编译器/解释器也)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
增量操作发生在for循环的末尾,因此它们将具有完全相同的输出。它不依赖于编译器/解释器。
jsternberg 2011年

@jsternberg ...谢谢,我不确定增量何时发生,因为我从未真正有理由进行过测试。自从我在大学里从事编译器以来,时间太长了!大声笑
肯尼斯(Kenneth)

错错错错。
ruohola

-1

我认为从语义++i上讲,它比有意义i++,所以我坚持第一个,除非通常不这样做(例如在Java中,i++因为它被广泛使用,所以应该在其中使用)。


-2

这不仅仅是性能。

有时您根本想避免实施复制,因为这没有意义。而且由于前缀增量的使用不依赖于此,因此坚持前缀形式显然更简单。

并为原始类型和复杂类型使用不同的增量……这确实难以理解。


-2

除非您真的需要它,否则我会坚持使用++ i。在大多数情况下,这就是我们的意图。您并不经常需要i ++,并且在阅读这样的结构时总是必须三思。使用++ i,这很容易:您添加1,使用它,然后我仍然一样。

因此,我同意@martin beckett的全心全意:让您自己更轻松,这已经足够难了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.