Saturday 6 January 2018

Devirtualization

When I came across this article a few weeks ago it quite caught me by surprise. They talk about a kind of optimization that I was not aware of and it has helped me to refresh and regain some knowledge, so I'll write down some notes here for further reference. I'll be talking about C#, but the most generic part applies the same to Java (and other static, class based, languages).

Virtual calls are resolved at runtime. Objects point to a VTable, where there is an entry for each virtual method in the class. This way the correct method, Base.Method or Child.Method is called depending on the runtime type of the object. When invoking a method through an interface the thing gets a bit more complicated. As a class can implement multiple interfaces it's no longer a matter of going always to the n slot in the VTable. Years ago I managed to understand how interfaces and their VTables were managed in .Net, but I no longer remember how it worked, and indeed I can read that it has changed (huge article) in the last implementations of the CLR. I'm quite impressed cause it reminds me a bit of how things work in Java with "dynamic"... well, all in all let's just stick with the idea that calling a (virtual) method through an interface is a bit slower than calling it through a class, and of course both are slower than calling non virtual methods, where the compiler can put a direct call rather than the indirection levels involved by the VTables. There are multiple articles about this. Delegate calls are also slower than direct calls, what I don't fully understand is why they are slightly slower than virtual calls (based on some benchmarks), I would say that the levels of indirection involved are just the same.

In those old days when I used to check and even write some IL code I found it odd that calls to instance, non virtual methods are done through the callvirt IL instruction (as for virtual methods) rather than using the call IL instruction (that is used mainly for static methods and structs). The reason for this is that callvirt provides an additional feature, it checks whether the object is null, which is obviously unnecessary for static methods, but much needed for instance methods, regardless of whether they are virtual or not. When the JIT translates a callvirt opcode into real machine code, it will for sure add the null check, and depending on whether the method is virtual or not it will emit code to use the VTable or will insert a direct call to the method address, as explained here. There is another case when the C# compiler can skip the null check and emit a call rather than a callvirt, when the method call is immediately after the object creation, i.e. new myClass().method();

Related to all this I can remember how one friend of mine used to seal his classes due to performance benefits, honestly, I never put much interest in such habit of him. I can read now that sealed classes were not created with performance in mind, but they can incidentally provide performance benefits, due to devirtualization preciselly.

The JITter will sometimes use non-virtual calls to methods in sealed classes since there is no way they can be extended further.

If we have a sealed, child class that either overrides or keeps the original base virtual methods, for the calls to that method done through the child class (not through the base one), given that we are preventing further inheritance we can be sure that the method to be called is the one in the child class, so there's no need to use the VTable at all. The JIT takes this into accout and generates native code that calls the method directly. It's explained in the first linked article:

  • Calling virtual methods on a sealed class.
  • Calling virtual methods on a sealed method.
  • Calling virtual methods when the type is definitely known (e.g. just after construction).

No comments:

Post a Comment