【问题标题】:Why can ++i ever be different from i+=1 performance-wise为什么 ++i 与 i+=1 性能不同
【发布时间】:2012-09-24 02:07:44
【问题描述】:

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

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

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

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


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

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

我并不热衷于这个具体的示例,而是或多或少地泛泛而谈。

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

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

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

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

【问题讨论】:

  • 嗯,这真的取决于i的类型。
  • 我认为这属于元数据。
  • 为什么还会存在诸如“i++ 是否比 i=i+1 快”之类的问题?
  • 因为人不是生来就有知识的,因为人是好奇的。
  • 也许它们的存在是因为你可以为这样一个愚蠢的问题获得 50 次投票...

标签: c++ c performance optimization


【解决方案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 编译器被美化为汇编器。

【讨论】:

  • 使用 1973 年的编译器技术肯定可以生成类似的代码(例如,参见大量引用的 design of an optimizing compiler,转换处于窥视孔优化阶段(BLISS 是一种或多或少相似的无类型语言)到 BCPL)这里描述的编译器以 PDP-11 为目标——作为第一个 C 编译器——但是在 PDP-10 上运行,这是一台更大的机器,意味着更多的资源可用。
  • 关于 1994 年编译器的状态,当时我开始在家中使用 Linux 进行论文工作。原因是它更容易与我在 uni 使用的 Sun 一起工作,避免了 win16/win32 的混乱,并且与我拥有的 Borland 和 Microsoft 编译器相比,性能提高了 2 倍。
  • @AProgrammer:同意,在 1972 年就已知技术可以进行优化,您可能会说这种潜力对于任何编译器编写者来说都是显而易见的,只要一个增量指令被发明。即便如此,我认为如果典型的编译器不使用这些技术,那么“你从 X 中获得性能优势”的说法是合理的。或者即使一个非典型但潜伏在那里的编译器没有,也许会用一些狡猾的话来形容“你有时会获得性能优势”,或者“你可能不会看到优势,但是这样做只是以防万一”。
【解决方案2】:

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

如果上述三个表达式中的任何一个带有i 的值都没有被使用,那么这三个是相同的。如果它是一个好的编译器,它可以优化出i++产生的未使用的临时变量。

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

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

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

如果,OTOH,您首先将 i 增加 1,然后再一次,您必须在某处(在寄存器或内存中)创建一个等于 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 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,屏幕是 32x24,甚至不是 80x25。

所有 80 年代初的 Small C 代码,几乎无法装入计算机的内存,并没有优化任何东西!

【讨论】:

  • FWIW,最初的 B 和 C 编译器实际上必须非常有选择地引入功能,因为在额外的聪明程度使编译器变得更大之间存在着严格的权衡, vs 额外的聪明使编译器在自己编译时小了多少。因此,一些改进是必不可少的,而另一些则是毁灭性的。当 Unix “升级”到 PDP-11 时,他们有 12k 用于操作系统,另外 12k 分配给用户程序和 RAM 磁盘。我假设编译器是一个用户程序。 cm.bell-labs.com/who/dmr/chist.html“更多历史”。
  • @SteveJessop 是的,这是人们必须考虑的权衡。
  • there were more good and brilliant programmers than there are today. 这是一个很好的观点,但我宁愿说那些日子里优秀程序员的百分比更高。现在有了 IDE,你可以通过拖动漂亮的图片来制作界面,并且可以自动生成一半的代码,有很多人进入编程领域,但并不想深入研究复杂的事情。我相信在那些日子里,甚至很难想象高级开发工具最终会变成什么样子。
  • @SingerOfTheFall 百分比就是我说的more good and brilliant programmers ... And that, of course, is not in absolute digits, but rather in relative.
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

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

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

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

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

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

    【讨论】:

    • OP 没有询问前缀与后缀的增量。我什至会说a++ 永远不会比++a 快。 (parashift.com/c++-faq-lite/increment-pre-post-speed.html)
    • 取决于它们的编写方式和课程。如果 i 是整数,我怀疑 ++i 比 i++ 快。
    • 正是如此,“a++ 永远不会比 ++a 更快甚至可能更慢”比“a++ 不太可能更快”要准确得多
    • 我可以为给定的类实现运算符,但事实并非如此。它不一定是一个好的实现,但它仍然是可能的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-10-16
    • 2012-08-25
    • 2015-08-17
    • 2022-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多