2

显然在阅读了旧标题之后

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

人们懒得彻底阅读问题本身。

问题在于人们提出这个问题的原因!++i这是关于为什么编译器会在和之间产生差异i+=1,以及是否存在任何可能的情况。虽然我感谢您所有诙谐而深刻的评论,但我的问题与此无关。


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

在 i=i+1 上使用 ++i 会给您带来性能优势。

我不热衷于这个特定的例子,而是或多或少地泛泛而谈。

显然,作者在写这本书时,对他来说是有道理的,他不只是虚构的。我们知道,现代编译器不关心您是否使用++i,i+=1i = i + 1,代码会被优化并且我们将有相同的 asm 输出。

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

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

仅仅是因为当时很难/不可能使编译器足够先进以执行此类优化吗?++i或者也许在某些非常特定的平台/硬件/在某些特殊情况下,在andi+=1和其他类似的东西之间做出区别实际上是有意义的?或者它可能取决于变量类型?还是编译器开发人员只是懒惰?

4

4 回答 4

12

想象一个非优化编译器。它真的不在乎是否++i等效i+=1,它只是发出它可以想到的第一件事。它知道 CPU 有一个加法指令,它知道 CPU 有一个递增整数的指令。所以假设i有 type 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 编译器被美化为汇编器。

于 2012-09-20T11:07:25.647 回答
9

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

i在未使用上述三个表达式 with 中的任何一个的情况下,这三个是相同的。如果它是一个好的编译器,它可以优化掉由i++.

这个临时变量之所以活跃,是因为i++它决定了以下两件事:

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

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

如果,OTOH,您首先增加i1,然后再次,您必须在某处(在寄存器或内存中)创建一个等于i-1撤消增量的值,因此可以获得旧的(预增加的)值作为结果表达式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 语言最重要的特性的一个非常简单且功能减少的 C 编译器。您会发现一些由Ron Cain, James Hendrix(80 年代初)和其他实现以及从这些实现(例如 RatC/Lancaster 由Bob Berry和实现相同Brian Meekings)。

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

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

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

于 2012-09-20T11:17:38.907 回答
2

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

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

于 2012-09-23T13:35:40.883 回答
1

答案取决于是什么类型i

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

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

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

  • a++不太可能比 快++a,因为它需要在递增之前返回对象的副本。
  • a = a + b不太可能比第一个更快,a += b因为第一个需要创建一个临时的。
  • a += 1那时不太可能更快,++a因为1可能与 a 的类型不同,并且可能会涉及一些费用并采取任何必要的措施来解决该问题。
  • 对于某些类,其中一些操作可能无论如何都不可用。

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

于 2012-09-20T10:55:44.803 回答