为什么++i在性能方面与i+=1不同

Why can ++i ever be different from i+=1 performance-wise

本文关键字:不同 方面 ++i 性能 为什么      更新时间:2023-10-16

显然是在阅读了的旧标题之后

为什么像is ++i fster than i+=1这样的问题甚至存在

人们没有仔细阅读这个问题本身。

问题是而不是关于人们提出这个问题的原因!这是关于为什么编译器会在++ii+=1之间产生差异,以及有没有任何可能的情况下这是有意义的。虽然我很欣赏你诙谐而深刻的评论,但我的问题并不是关于它


好吧,让我试着用另一种方式来回答这个问题,我希望我的英语足够好,这次我可以表达自己而不会被误解,所以请阅读。假设有人在一本10年前的书中读到了这一点:

使用++i而不是i=i+1会给您带来性能优势。

我不喜欢这个特定的例子,而是或多或少地笼统地谈论。

显然,当作者写这本书时,这对他来说是有意义的,他不仅仅是虚构的。我们知道,现代编译器不在乎你是使用++ii+=1还是i = i + 1,代码会得到优化,我们会得到相同的asm输出。

这似乎很合乎逻辑:如果两个操作做相同的事情,并且有相同的结果,就没有理由将++i编译成一个东西,将i+=1编译成另一个东西。

但是自从作者写了这本书之后,他就看到了区别!这意味着某些编译器实际上会为这两行生成不同的输出。这意味着,制作编译器的人有理由区别对待++ii+=1我的问题是他们为什么会这么做?

这仅仅是因为在那些日子里,很难/不可能让编译器变得足够高级来执行这样的优化吗?或者,在一些非常特定的平台/硬件上/在一些特殊的场景中,区分++ii+=1以及其他类似的东西实际上是有意义的?或者它可能取决于变量类型?或者编译器开发人员只是懒惰?

想象一个非优化编译器。它真的不在乎++i是否等同于i+=1,它只是发出它能想到的第一个有效的东西。它知道CPU有一条加法指令,它知道CPU也有一条递增整数的指令。因此,假设i的类型为int,那么对于++i,它会发出类似于的信号

inc <wherever_i_is>

对于i+=1,它会发出类似于的信号

load the constant 1 into a register
add <wherever_i_is> to that register
store that register to <wherever_i_is>

为了确定后一个代码"应该"与前一个代码相同,编译器必须注意添加的常量是1,而不是2或1007。这需要编译器中的专用代码,标准不需要,而且不是每个编译器都能做到

所以你的问题相当于,"既然我发现了这种等价性,但它没有,为什么编译器会比我更笨?"。答案是,现代编译器在很多时候都比你聪明,但并非总是如此,也并非总是如此。

自从作者写了这本书以来,他就看到了的差异

不一定。如果你看到一个关于什么是"更快"的声明,有时这本书的作者比你和编译器都要笨。有时他很聪明,但在不再适用的条件下,他聪明地形成了自己的经验法则。有时,他会猜测是否存在像我上面描述的那样愚蠢的编译器,而没有真正检查你真正使用过的编译器是否真的那么愚蠢。就像我刚才做的那样;-)

顺便说一句,对于一个启用了优化的像样的编译器来说,10年前还太晚了,不能不进行这种特定的优化。确切的时间可能与你的问题无关,但如果一位作者写了这篇文章,而他们的借口是"那是在2002年",那么我个人不会接受。当时的说法并不比现在更正确。如果他们说1992年,那么好吧,就我个人而言,我不知道当时的编译器是什么样的,我不能反驳他们。如果他们说1982年,那么我仍然会怀疑(毕竟,C++是在那时发明的。它的大部分设计都依赖于优化编译器,以避免运行时大量浪费工作,但我承认,这一事实的最大用户是模板容器/算法,这在1982年是不存在的)。如果他们说1972年,我可能会相信他们。当然,有一段时期,C编译器被美化为汇编程序。

在C中,i++通常不等同于i=i+1,因为两者产生不同的表达式值。CCD_ 21与CCD_ 22等价,因为它们产生相同的表达式值。

在不使用具有i的上述三个表达式中的任何一个的值的情况下,这三个表达式是相同的。如果它是一个好的编译器,它可以优化出i++生成的未使用的临时变量。

由于i++规定了以下两件事,这个临时变量开始活跃起来:

  1. i的原始值由表达式i++返回
  2. i增加1

如果先取i的原始值,然后递增i,那么i的原始值(现在是旧值)必须存在于某个地方(内存或寄存器,无关紧要),因为它不能存在于现在递增的变量i中。这是你的临时变量。

如果,OTOH,您首先将i递增1,然后再次,您必须在某个位置(在寄存器或内存中)创建一个等于i-1的值来撤消递增,因此旧的(pred递增的)值可以作为表达式i++的结果获得。

有了++ii=i+1,事情就简单多了。这些表达要求两件事:

  1. i递增
  2. 返回CCD_ 39的新值

这里自然是先递增i,然后取其值。您不必有一对值ii+1(或i-1i),旧的和新的。我们在这里只需要新的。

现在,当编译器不太擅长优化时,周围有一些旧书和老人。从那里可以得出i++可能比++i慢的想法。这种差异是在实践中观察到的,而不是弥补的。这是真实的,有些人可能认为今天仍然如此。

还可以尝试分析两(三)个递增表达式之间的差异,并发现在i++的情况下,确实可能需要进行一些额外的操作并为临时变量使用额外的存储单元。在这一点上,这个人可能看不到什么时候不需要这个临时的,也看不到如何检测它是否有必要。这是关于上述差异问题的另一种可能性。

当然,人们一直都喜欢巨魔

至于编译器开发人员懒惰。。。我不认为他们是。原因如下。

在过去,计算机比现在慢得多,RAM也少得多。

即使在那时,编写一个像样的优化编译器也是可能的。

问题是,用于优化的额外代码使编译器明显变得更大、更慢。如果它更大,可以运行它的计算机就会更少,可以使用它编译代码的程序员就会更少。如果它比其他编译器慢,人们会更喜欢其他编译器,因为人们讨厌坐着等待。

举个例子:我。我确实在90年代中期访问了Borland的Turbo C/C++。但直到90年代末、0年代初,我才考虑学习和使用C。原因是什么?Borland的C/C++比他们的Pascal慢得多,我的电脑也不是一台好电脑。等待代码编译是痛苦的。事情就是这样。我最初掌握了Pascal,后来才回到C和C++。

因此,更智能、更大、更慢的编译器正在花费编译器用户的金钱和时间。至少在主动开发期间,这仍然是一个非常重要的产品阶段,即使最终产品是用不同的编译器编译的。

你也不应该忘记,用当时的基本工具开发和管理一大块编译器代码也不是很有趣。直到现在,你才有了一个漂亮的IDE(而不是一个!),里面有调试器,语法高亮显示,自动完成等等,源代码管理,简单的文件比较,Internet/StackOverflow等等……我们现在可以有多个20多英寸的显示器连接到PC!现在我们谈论的是生产力!:)

真的,我们今天有很棒的工具和设备。20、30、40年前,人们只能想象或预测它们,但还没有使用。

情况更为艰难。而且,虽然我不打算在这里发表声明,但我不会惊讶地发现,在编程不如现在商品化的时候,有比今天更多优秀的程序员。当然,这不是绝对数字,而是相对数字。

所以,我怀疑编译人员是懒惰的。

在网上查找所谓的Small C。它是一个通用术语,指的是只实现C语言最重要功能的C编译器。你会发现Ron CainJames Hendrix(80年代早期)和其他的一些实现以及它们的衍生物(例如,Bob BerryBrian Meekings的RatC/Lancaster实现)。

如果你查看这些Small C's中的任何一个的代码,你会发现最小代码大小大约是50+KB,带有2+KLOC,这只是从C到汇编代码的转换器!在某个时刻,有些人需要用汇编程序来汇编它。

我无法想象在8位家用电脑上轻松地完成这样的项目,例如ZX Spectrum(我小时候就有它),它的RAM最大可达48KB,CPU运行频率约为3MHz,所有存储都在录音机上,数据传输速率约为10KB/分钟,屏幕为32x24,甚至不是80x25。

80年代初的所有小C代码,几乎不适合计算机的内存,没有优化任何东西!

我不完全确定你指的是哪本书,因此无法查找原始引文。然而,我怀疑作者并没有完全谈论内置类型。对于内置类型,表达式++ii += 1i = i + 1是等效的,编译器很可能会选择最有效的一个,但对于其他类型,例如任何随机访问迭代器,它们不一定是等效的。从语义上讲,它们是等价的,但编译器没有这种语义知识,而且实现可能会做不同的事情。习惯于编写在使用类类型的对象时可能最有效的表单,即使是在使用内置类型时,也可以避免不必要的性能问题:您使用的是最有效的"自动"方式,因此无需太多关注。

当定义一个提供相关运算符的类时,例如,当创建随机访问迭代器时,编译器可能无法确定代码是否等效。其中一个原因是代码不一定是可见的,例如,当函数没有内联时。即使函数是内联的,也可能存在编译器无法跟踪的副作用。随机访问迭代器的实现可以很好地在内部使用指针并使用++pp += n。然而,当n恰好是值为1的常数的信息丢失时,它就不能再用++p代替p += n了。尽管编译器擅长常量折叠,但它至少要求整个代码是内联的,并且编译器已经决定内联函数确实应该内联。

答案取决于i是什么类型。

当实现该类时,有不同的运算符用于递增前(T & T::operator++()、递增后(T T::operator ++(int))、加法(T T::operator +(T const &)等))和递增(T T::operator +=(T const &))。(显然,所有这些都有变体)

对于足够琐碎的类型,这些可能都很重要。

然而,对于非平凡类型,性能将取决于它们的编写方式。一般情况下:

  • a++不太可能比++a快,因为它需要在递增之前返回对象的副本
  • CCD_ 70不太可能比CCD_
  • a += 1不太可能比++a快,因为1可能与a不是同一类型,并且可能会涉及一些费用,并采取一切必要措施来解决此问题
  • 对于某些类,其中一些操作可能无论如何都不可用

除此之外,您唯一可以确定的是,您应该查看代码并运行性能测试。