6

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

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

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

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

4

4 回答 4

9

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

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

至少,这是我的直觉。至少有一个实际从事优化器工作的人告诉我,这会产生重大影响,所以我不会说我对此很确定。

编辑:

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

于 2012-07-12T10:50:56.620 回答
3

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

于 2012-07-12T11:01:58.413 回答
1

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

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

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

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

于 2012-07-12T16:13:31.930 回答
0

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

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

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

正如您从图片中看到的那样,有很多潜在的并行性,现代编译器可以通过没有规定的评估顺序来获得一些东西。

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

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

于 2012-07-12T12:05:19.870 回答