As others say, you should measure your program's performance first, and will probably find no difference in practice.
Still, from a conceptual level I thought I'd clear up a few things that are conflated in your question. Firstly, you ask:
Do function call costs still matter in modern compilers?
Notice the key words "function" and "compilers". Your quote is subtley different:
Remember that the cost of a method call can be significant, depending on the language.
This is talking about methods, in the object oriented sense.
Whilst "function" and "method" are often used interchangably, there are differences when it comes to their cost (which you're asking about) and when it comes to compilation (which is the context you gave).
In particular, we need to know about static dispatch vs dynamic dispatch. I'll ignore optimisations for the moment.
In a language like C, we usually call functions with static dispatch. For example:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
When the compiler sees the call foo(y)
, it knows what function that foo
name is referring to, so the output program can jump straight to the foo
function, which is quite cheap. That's what static dispatch means.
The alternative is dynamic dispatch, where the compiler doesn't know which function is being called. As an example, here's some Haskell code (since the C equivalent would be messy!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Here the bar
function is calling its argument f
, which could be anything. Hence the compiler can't just compile bar
to a fast jump instruction, because it doesn't know where to jump to. Instead, the code we generate for bar
will dereference f
to find out which function it's pointing to, then jump to it. That's what dynamic dispatch means.
Both of those examples are for functions. You mentioned methods, which can be thought of as a particular style of dynamically-dispatched function. For example, here's some 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)
The y.foo()
call uses dynamic dispatch, since it's looking up the value of the foo
property in the y
object, and calling whatever it finds; it doesn't know that y
will have class A
, or that the A
class contains a foo
method, so we can't just jump straight to it.
OK, that's the basic idea. Note that static dispatch is faster than dynamic dispatch regardless of whether we compile or interpret; all else being equal. The dereferencing incurs an extra cost either way.
So how does this affect modern, optimising compilers?
The first thing to note is that static dispatch can be optimised more heavily: when we know which function we're jumping to, can do things like inlining. With dynamic dispatch, we don't know we're jumping until run time, so there's not much optimisation we can do.
Secondly, it's possible in some languages to infer where some dynamic dispatches will end jumping to, and hence optimise them into static dispatch. This lets us perform other optimisations like inlining, etc.
In the above Python example such inference is pretty hopeless, since Python allows other code to override classes and properties, so it's difficult to infer much that will hold in all cases.
If our language lets us impose more restrictions, for example by limiting y
to class A
using an annotation, then we could use that information to infer the target function. In languages with subclassing (which is almost all languages with classes!) that's actually not enough, since y
may actually have a different (sub)class, so we'd need extra information like Java's final
annotations to know exactly which function will get called.
Haskell isn't an OO language, but we can infer the value of f
by inlining bar
(which is statically dispatched) into main
, substituting foo
for y
. Since the target of foo
in main
is statically known, the call becomes statically dispatched, and will probably get inlined and optimised away completely (since these functions are small, the compiler is more likely to inline them; although we can't count on that in general).
Hence the cost comes down to:
- Does the language dispatch your call statically or dynamically?
- If it's the latter, does the language allow the implementation to infer the target using other information (e.g. types, classes, annotatations, inlining, etc.)?
- How aggressively can static dispatch (inferred or otherwise) be optimised?
If you're using a "very dynamic" language, with lots of dynamic dispatch and few guarantees available to the compiler, then every call will incur a cost. If you're using a "very static" language, then a mature compiler will produce very fast code. If you're in between, then it can depend on your coding style and how smart the implementation is.
for(Integer index = 0, size = someList.size(); index < size; index++)
instead of simplyfor(Integer index = 0; index < someList.size(); index++)
. Just because your compiler was made in the last few years doesn't necessarily mean you can forego profiling.main()
, others split everything into some 50 tiny functions, and all are utterly unreadable. The trick is, as always, to strike a good balance.