定义的参数求值顺序会导致次优代码

Defined argument evaluation order leads to sub-optimal code?

本文关键字:代码 顺序 参数 定义      更新时间:2023-10-16

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

15.7.4.从左到右评估参数列表

在方法或构造函数调用或类实例创建表达式中,参数表达式可能出现在括号内,用逗号分隔。每个自变量表达式似乎在任何参数表达式的任何部分之前进行了完全求值这是正确的。如果参数表达式的求值突然完成,则任何参数的任何部分都不会右侧的表达式似乎已被求值?

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

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

这可能没有通常认为的那么重要。编译器可以如果表达式没有副作用,或者重新排序不会改变程序的可观察行为。现代编译器能够比编译器更好地确定这一点20多年前(C++规则制定时)。和大概,当他们无法确定这一点时,你已经做得够多了在每一个表达式中,额外的溢出到内存并不重要。

至少,这是我的直觉。至少有一个人告诉过我谁真正在优化器上工作,这将使差异,所以我不会说我对此很确定。

编辑:

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

Java假定了一个基于堆栈的虚拟机,在该虚拟机中对操作数进行重新排序没有任何好处。根据James Kanze的回答,C和大多数完全编译的语言都采用了寄存器体系结构,在这种体系结构中,寄存器"溢出"到内存的代价很高,而且非常需要避免,因此最好对操作数进行重新排序,实际上是做各种事情,以最大限度地提高寄存器的使用率,最大限度地减少溢出。

我认为我们对它进行了过度分析。真正的答案是可能在C标准之前的旧时代,当K&R是事实上的标准,没有人愿意指定在哪个顺序中评估参数,不同的编译器实现采用不同的方式。

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

因此,标准编写者面临着一个选择,即以"正确"的方式(从左到右)进行编写,可能会破坏很多代码,或者以大多数现存编译器的方式进行编写,也可能会破坏其他代码,或者坚持现状,让编译器设计者选择要做什么。

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

不适用于您的示例中的函数求值,但对于简单表达式,这两个表达式甚至可以并行执行。现代体系结构是流水线式的,(几乎)同时馈送两个流水线可以更有效,从而使必须执行的操作重叠。

此外,您似乎认为只有两个表达式可用于评估自变量,但有四个:aba()b()。函数调用的部分顺序是从左到右

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

正如你从图中看到的,有很多潜在的并行性,现代编译器可以通过没有指定的求值顺序来获得一些东西。

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

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