Someone recently asked me about devirtualization optimizations: when do they happen? when can we rely on devirtualization? do different compilers do devirtualization differently? As usual, this led me down an experimental rabbit-hole. The answer seems to be: Modern compilers devirtualize calls to final methods pretty reliably. But there are many interesting corner cases — including some I haven’t thought of, I’m sure! — and different compilers do catch different subsets of those corner cases.
First, let’s observe that devirtualization can (probably?) be done more effectively via LTO, using whole-program analysis. I don’t know anything about the state of the art in link-time devirtualization, and it’s hard to experiment with on Compiler Explorer, so I’m not going to talk about LTO at all. We’re looking purely at what the compiler itself can do.
There are basically two situations where the compiler knows enough to devirtualize. They don’t have much in common:
When we know the instance’s dynamic type
The archetypical case here is
void test() { Apple o; o.f(); }
It doesn’t matter if Apple::f is virtual; all virtual dispatch ever does is invoke the method on the actual dynamic type of the object, and here we know the actual dynamic type is exactly Apple . Static and dynamic dispatch should give us the same result in this case.
A sufficiently smart compiler will use dataflow analysis to optimize non-trivial cases such as
Derived d; Base *p = &d; p->f();
It turns out that even this simple dodge is enough to fool MSVC and ICC. The next test case is
... continue reading