【问题标题】:Defined argument evaluation order leads to sub-optimal code?定义的参数评估顺序导致次优代码?
【发布时间】:2012-07-12 10:40:52
【问题描述】:

众所周知,c 和 c++ 中的参数评估顺序没有定义: 例如:foo(a(),b()) 在上面的调用中,由编译器的实现来决定选择哪个评估顺序,然后决定首先执行哪个函数。最近我的一位朋友问为什么在 C 或 C++ 中未指定评估顺序。当我搜索它时,我开始知道指定评估顺序会导致代码生成不理想。但怎么会这样呢?为什么定义的参数评估顺序会导致次优代码?当我提到 Java 的参数评估顺序时。我在规范中找到了以下内容。

15.7.4。参数列表从左到右求值

在方法或构造函数调用或类实例创建表达式中, 参数表达式可能出现在括号内,用逗号分隔。每个论点 表达式似乎在任何参数表达式的任何部分之前被完全评估 这是正确的。 如果参数表达式的评估突然完成,则任何参数都没有 其右侧的表达式似乎已被评估?

既然如此,Java 有一个定义的参数评估顺序,但是如果指定这样的行为,说 C 或 C++ 编译器会产生次优代码似乎有点奇怪。你能解释一下吗?

【问题讨论】:

    标签: java c++ c evaluation


    【解决方案1】:

    这部分是历史性的:在寄存器很少的处理器上,对于 例如,一种传统(且简单)的优化技术是 首先评估需要最多寄存器的子表达式。如果一个 子表达式需要 5 个寄存器,另外 4 个,例如,你 可以将需要5的结果保存在不需要的寄存器中 由需要 4 的那个。

    这可能与通常认为的不太相关。编译器可以 如果表达式没有副作用,则重新排序(即使在 Java 中),或者 重新排序不会改变程序的可观察行为。 现代编译器能够比编译器更好地确定这一点 二十多年前(当时制定了 C++ 规则)。和 据推测,当他们无法确定这一点时,您已经做得足够了 在每个表达式中,额外的内存溢出无关紧要。

    至少,这是我的直觉。至少有人告诉过我 谁实际上在优化器上工作,这将产生重大影响 区别,所以我不会说我确定。

    编辑:

    只是添加一些关于 Java 模型的 cmets。当 Java 是 在设计中,它被设计为一种解释语言。极端 性能不是问题;目标是极度安全,并且 可复制性。因此,它非常精确地指定了许多东西,所以 任何编译的程序都将具有完全相同的行为 与平台无关。应该没有未定义的 行为,没有实现定义的行为,也没有未指定的 行为。不计成本(但相信这可能是 在任何最普遍的机器上以合理的成本完成)。一 C(以及间接的 C++)的初始设计目标是不必要的 额外的运行时成本应该是最低的,即平台之间的一致性 不是目标(从那时起,即使是常见的平台也各不相同 很大),而安全性虽然令人担忧,但并不是最重要的。尽管 态度变了一些,还是有目标的 有效地支持任何可能存在的机器。没有 需要最新、最复杂的编译器技术。而且不同 目标自然会导致不同的解决方案。

    【讨论】:

    • 在某些情况下,例如获取参数 3 的内存,然后计算 args 0,1,2 的值,完成后,参数 3 的内存已到达。
    • @PlasmaHH 好点。但在所有这些情况下,我认为编译器应该能够确定它不会影响“可观察行为”。
    • 所以本质上,如果 C 和 C++ 编译器更出色并且使用现代机器,那么它们也将具有定义的参数评估顺序??
    • @sasidhar 可能有些编译器会这样做。但标准并不要求它,因此编译器编写者不会做出任何特别努力来确保它。
    • @JamesKanze:当然可以,但是如果会影响可观察的行为(例如,因为计算存储了一个新值)怎么办?那么它就无法应用该优化。 C 和 C++ 在允许优化方面做出了权衡。
    【解决方案2】:

    Java 提出了一个基于堆栈的虚拟机,在其中重新排序操作数没有任何优势。根据 James Kanze 的回答,C 和大多数完全编译的语言都假定了一个寄存器架构,其中寄存器“溢出”到内存是昂贵的,并且可以大大避免,因此最好重新排序操作数,实际上是做各种东西,以最大限度地提高寄存器使用率并最大限度地减少溢出。

    【讨论】:

    • 不是重点。毕竟 JIT 也必须遵循 JLS 并且编译为大多数架构的寄存器。如果允许的话,那里可能有同样的优势,但我怀疑它在任何地方都很明显。要么函数是内联的,在这种情况下编译器可以重新排序,而不是真正的竞争条件,或者它们足够大或未知,编译器可能无论如何都不会查看它们(对于 IPO,它可能会有所作为)。
    • @Voo 重点是设计语言时设计师的想法。
    • 嗯,可能不确定在创建第一稿时 JIT 是否已经在桌面上。我是这么认为的,但考虑到第一个 JIT 出现所花费的时间,实际上可能会在以后出现。
    • @Voo 第一个 JIT 是商业产品,在 Java 发布之前不可能也不会出现,而且 Java 在首次发布之前就已经设计好了。
    • 并不意味着设计者没有考虑规范对可能的性能优化的影响。 JIT 在当时并不新鲜,解释代码不是最快的事实当然也不足为奇。例如。那时多线程代码非常不寻常,他们仍然以这样一种方式编写规范,以便您可以在面对多线程时很好地优化代码。
    【解决方案3】:

    我认为我们过度分析了它。真正的答案是可能,在 C 标准之前的日子里,当 K&R 成为事实上的标准时,没有人费心指定评估参数的顺序,并且不同的编译器实现以不同的方式进行。

    从人类的角度来看,逻辑的方法是从左到右评估参数(正如 Java 所做的那样)。从编译器的角度来看,简单的方法是从右到左进行参数评估。这样一来,一旦对参数进行了评估,就不需要将其保存在任何地方,它可以被压入堆栈以供调用。大多数使用堆栈作为参数的 C 实现需要以相反的顺序推送它们。这是因为如果一个函数没有在同一个源文件中定义,并且程序员过去常常利用它来提供可变参数函数的原始形式,K&R C 就无法让编译器计算出一个函数需要多少个参数。

    因此,标准编写者面临着选择以“正确”方式(从左到右)执行它,并且可能会破坏大量代码,或者按照大多数现存编译器的方式执行它,并且可能会破坏一些其他代码或者坚持现状,让编译器设计者选择做什么。

    无论如何,这是我的观点,不基于任何事实。

    【讨论】:

      【解决方案4】:

      不适用于您的示例中的函数评估,但对于简单的表达式,这两个表达式甚至可以并行执行。现代架构是流水线的,同时(几乎)同时提供两条流水线会更有效,这样必须执行的操作重叠。

      此外,您似乎认为只有两个表达式可以评估参数,但有四个:aba()b()。函数调用总的偏序是从左到右

          a -- a()
                   \
               f --- f(a(), b())
                   /
          b -- b()
      

      从图中可以看出,存在很多潜在的并行性,现代编译器可以通过没有规定的评估顺序来获得一些好处。

      编辑:鉴于讨论的一些额外细节。如果a()b() 是函数调用,则标准保证这些函数调用不会交错,即使函数是内联的。那么上面的图片应该有一个附加约束,a()b() 必须以某种方式排序。 (我不知道怎么把它放在图片中。)

      另一方面,如果它们是其他表达式,例如宏求值,那么如果有增益,这些表达式可能(并且可能会)交错。

      【讨论】:

      • 如果 C 编译器开始使用这样的并行性,那么 a()b() 中的代码不应该是线程安全的吗?一般来说,在编写 C 代码时,我们并没有太多考虑并行性,对吧?
      • 如果编译器绝对确定不存在竞争条件,我们只能并行执行这两个函数(否则我们可能会得到一个有趣的混合状态,这也是 c 不允许的)。如果编译器能够弄清楚这一点,它就可以并行评估这些函数。
      • @Voo C 编译器从什么时候开始发现这一点的??线程概念不是 C 的核心,只能通过库来实现吧?
      • @sasidhar 如果两个函数都足够小,它们将被内联,然后可以进行通常的优化,包括重新排序(我们也可以自动并行化代码,只要结果在 c 模型下有效) .在这种情况下,如果这两个功能具有优势,那么这两个功能是准并行执行的(至少是相互交织的)。一般情况下?有首次公开募股,但不知道这对它有多大的作用——我猜不太好。
      • 不仅仅是典型意义上的竞争条件。 C 保证在两次调用a()b() 之间,每个人都看到对方的所有副作用,或者看不到任何副作用。这也对在另一个线程中运行的代码产生影响,并且可能会看到 a 和 b 的效果,只要它对这些效果的访问以某种方式同步。
      猜你喜欢
      • 1970-01-01
      • 2018-11-05
      • 2011-12-17
      • 2013-03-18
      • 1970-01-01
      • 2014-04-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多