就像其他人所说的那样,您应该首先衡量程序的性能,并且在实践中可能不会发现任何不同。
不过,从概念上讲,我认为我将清除一些与您的问题混为一谈的内容。首先,您问:
函数调用成本在现代编译器中是否仍然重要?
注意关键字“功能”和“编译器”。您的报价与众不同:
请记住,方法调用的成本可能很高,具体取决于语言。
这是在面向对象的意义上谈论方法。
尽管“功能”和“方法”通常可以互换使用,但是它们的成本(您要询问的)和编译时(即您所提供的上下文)的成本是有所不同的。
特别是,我们需要了解静态调度与动态调度。我暂时将忽略优化。
在像C这样的语言中,我们通常使用static dispatch调用函数。例如:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
当编译器看到该调用时foo(y)
,它知道该foo
名称指的是哪个函数,因此输出程序可以直接跳转到该foo
函数,这非常便宜。这就是静态调度的意思。
另一种选择是动态调度,其中编译器不知道正在调用哪个函数。作为示例,下面是一些Haskell代码(因为等效的C语言很杂乱!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
在这里,bar
函数正在调用其参数f
,该参数可以是任意值。因此,编译器不能只编译bar
为快速跳转指令,因为它不知道跳转到哪里。相反,我们为其生成的代码bar
将取消引用f
以找出其指向的功能,然后跳转到该功能。这就是动态调度的意思。
这两个例子都是针对函数的。您提到了method,可以将其视为动态调度函数的一种特殊样式。例如,下面是一些Python:
class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
该y.foo()
调用使用动态调度,因为它foo
在y
对象中查找属性的值,并调用找到的所有内容;它不知道y
会有class A
,或者A
该类包含一个foo
方法,所以我们不能直接跳转到它。
好,那是基本思想。需要注意的是静态调度比动态调度快,无论我们是否编译或解释; 其他所有条件都一样。无论哪种方式,取消引用都会产生额外的费用。
那么,这如何影响现代的,优化的编译器?
首先要注意的是,静态分配可以得到更大程度的优化:当我们知道我们要跳转到哪个函数时,可以执行内联之类的事情。使用动态调度,我们不知道要等到运行时才跳,所以我们可以做的优化不多。
其次,在某些语言中,可以推断一些动态调度将结束跳转到的位置,从而将它们优化为静态调度。这使我们可以执行其他优化,例如内联等。
在上面的Python示例中,这种推论是完全没有希望的,因为Python允许其他代码覆盖类和属性,因此很难推论在所有情况下都适用的内容。
如果我们的语言允许我们施加更多限制,例如通过使用注释限制y
类A
,那么我们可以使用该信息来推断目标函数。在具有子类化的语言(几乎是所有具有类的语言!)中,这实际上是不够的,因为y
实际上可能具有不同的(子)类,因此我们需要Java final
注释之类的额外信息才能确切知道将调用哪个函数。
Haskell不是OO语言,但是我们可以f
通过内联bar
(静态分派)到main
,代替foo
来推断的值y
。由于foo
in 的目标main
是静态已知的,因此该调用将被静态分派,并且可能会内联并完全被优化(由于这些函数很小,编译器更可能内联它们;尽管我们通常不能指望它们) )。
因此,成本下降为:
- 语言是静态还是动态调度您的呼叫?
- 如果是后者,那么该语言是否允许实现使用其他信息(例如类型,类,注释,内联等)来推断目标?
- 静态调度(推断或其他方式)的优化程度如何?
如果您使用的是“非常动态”的语言,并且具有很多动态分配,并且编译器无法使用任何保证,那么每次调用都会产生成本。如果您使用的是“非常静态”的语言,那么成熟的编译器将产生非常快速的代码。如果介于两者之间,则可能取决于您的编码样式以及实现的聪明程度。