为什么++我可能与i + = 1表现不同

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

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

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

问题在于人们提出要求的理由! 这就是为什么编译器会在++ii+=1之间产生差异,并且有任何可能出现的情况都是有意义的。 虽然我很欣赏你所有的诙谐和深刻的评论,但我的问题并非如此。


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

使用++ i over 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  

对于i+=1 ,它会发出如下内容:

 load the constant 1 into a register add  to that register store that register to  

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

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

自书作者撰写以来,他已经看到了它的不同之处

不必要。 如果你看到关于什么是“更快”的声明,有时候这本书的作者比你和编译器都要笨重。 有时他很聪明,但他巧妙地在不再适用的条件下形成了他的经验法则。 有时他推测编译器的存在就像我上面描述的那样愚蠢,而没有真正检查你实际使用的编译器是否真的那么愚蠢。 就像我刚刚做的那样;-)

Btw,10年前对于一个优化启用的合适编译器来说太近了,不能进行这种特殊的优化。 确切的时间尺度可能与你的问题无关,但如果一位作者写道并且他们的借口是“那可以追溯到2002年”,那么我个人不会接受它。 声明当时没有比现在更正确。 如果他们说1992年那么好,我个人不知道那时编译器是什么样的,我不能反驳他们。 如果他们说1982年那么我仍然会怀疑(毕竟,C ++已经发明了。它的大部分设计都依赖于优化编译器,以避免在运行时浪费大量的工作,但我会批准这个事实的最大用户是模板容器/算法,它在1982年不存在)。 如果他们说1972年,我可能只是相信他们。 肯定有一段时间C编译器是美化assembly工。

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

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

这个临时变量源于生命,因为i++规定了以下两件事:

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

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

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

使用++ii=i+1事情要简单得多。 这些表达式要求两件事:

  1. i得到了增加
  2. 返回i的新值

在这里,首先增加i然后取其值是很自然的。 您不必拥有一对ii+1 (或i-1i ),新旧两种值。 新的就是我们所需要的。

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

人们还可以尝试分析两个(三个)递增表达式之间的差异,并且看到确实可能需要做一些额外的操作,并且在i++情况下使用额外的存储器单元用于临时变量。 此时此人可能无法确定何时不需要此临时措施或如何检测是否有必要。 对于上述差异的问题,这是另一种可能性。

当然,人们一直都喜欢拖钓。 🙂

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

回到过去,计算机比现在慢得多,而且它们的RAM也少得多。

即使这样,也可以编写一个不错的优化编译器

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

例证:我。 我在90年代中期确实可以访问Borland的Turbo C / C ++。 但直到90年代末,0年代早期,我才考虑学习和使用C. 原因? Borland的C / C ++比他们的Pascal慢得多,而我的PC也不是很好。 等待代码编译是痛苦的。 这就是它的样子。 我第一次掌握了Pascal,后来又回到了C和C ++。

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

您不应该忘记使用当时的基本工具开发和管理大量编译器代码也不是很有趣。 现在只有你可以拥有一个漂亮的IDE(而不是一个!),其中包含一个调试器,语法高亮,自动完成和所有,源代码管理,简单的文件比较,Internet / StackOverflow等等…我们可以现在有多个20+“显示器连接到PC!现在我们说的是生产力!:)

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

事情变得更加艰难。 而且,虽然我不打算在这里发表声明,但是当我发现编程没有像现在那样商品化时,我不会感到惊讶,那里有比现在更好的优秀程序员。 当然,这不是绝对数字,而是相对而言。

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

在网上查找名为Small C 。 它是一个通用术语,用于实现C语言最重要特性的function简化和function减少的C编译器。 你会发现Ron CainJames Hendrix (80年代早期)和其他人的衍生工具(例如由Bob BerryBrian Meekings执行的RatC / Lancaster)。

如果你查看任何那些Small C's代码,你会发现最小代码大小大约是50+ KB和2+ KLOC,而且这只是从C到汇编代码的翻译! 有人在某个时候需要用汇编程序组装它。

我无法想象在像8位家用计算机这样的项目上舒适地工作,例如ZX-Spectrum(我小时候就有),最大可能有48KB RAM,CPU运行在~3MHz ,所有存储都在录音机上,数据传输速率为10KB / min,屏幕为32×24,甚至不是80×25。

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

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

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

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

当实现类时,有不同的运算符用于预增量( T & T::operator++() ,后增量( TT::operator ++(int)) ,加法( TT::operator +(T const &) (和其他))和增量( TT::operator +=(T const &) )。(显然所有这些都有变体)

对于足够微不足道的类型,这些可能都非常多。

但是,对于非平凡类型,性能将取决于它们的编写方式。 一般来说:

  • a++不太可能比++a更快,因为它需要在递增之前返回对象的副本。
  • a = a + b不太可能比a += b快,因为第一个需要创建一个临时的。
  • a += 1不可能比++a更快,因为1可能与a不是同一类型,并且可能涉及一些费用并做任何必要的事情来解决这个问题。
  • 对于某些类,其中一些操作可能无法使用。

除此之外,你可以肯定地说,你应该检查代码并运行性能测试。