Virtual Function Performance
One type of function was left out of Unity Function Performance: virtual functions. Functions in C# are non-virtual by default and you have to explicitly use the virtual
and override
keywords to override them. Why not make this the default, like in AS3 or Java? Are virtual functions that much slower? Today’s article finds out! Should you be worried every time you make a function virtual
?
For some background on virtual functions, see this article in the From AS3 to C# series that covers how they work. For some behind the scenes information on how virtual functions are implemented, check out the Wikipedia article on dynamic dispatch.
In short, here’s what we’re testing today:
public class Parent { // Virtual function in a parent class public virtual void VirtualFunction() { } // Non-virtual function in a parent class public void NonVirtualFunction() { } } public class Child : Parent { // Virtual function in a child class overriding a // virtual function in the parent class public override void VirtualFunction() { } // Non-virtual function in a child class with the // same name as the non-virtual function in the // parent class public new void NonVirtualFunction() { } }
And here’s the test script that compares the performance of these four function types.
using UnityEngine; public class Parent { public virtual void VirtualFunction() { } public void NonVirtualFunction() { } } public class Child : Parent { public override void VirtualFunction() { } public new void NonVirtualFunction() { } } public class TestScript : MonoBehaviour { private const int NumIterations = 100000000; private string report; void Start() { var stopwatch = new System.Diagnostics.Stopwatch(); var parent = new Parent(); var child = new Child(); stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < NumIterations; ++i) { parent.VirtualFunction(); } var virtualParentTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < NumIterations; ++i) { child.VirtualFunction(); } var virtualChildTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < NumIterations; ++i) { parent.NonVirtualFunction(); } var nonVirtualParentTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < NumIterations; ++i) { child.NonVirtualFunction(); } var nonVirtualChildTime = stopwatch.ElapsedMilliseconds; report = "Test,Virtual Time,Non-Virtual Time\n" + "Parent," + virtualParentTime + "," + nonVirtualParentTime + "\n" + "Child," + virtualChildTime + "," + nonVirtualChildTime; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } }
If you want to try out the test yourself, simply paste the above code into a TestScript.cs
file in your Unity project’s Assets
directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.3
- Unity 5.0.2f1, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Virtual Time | Non-Virtual Time |
---|---|---|
Parent | 235 | 31 |
Child | 224 | 30 |
When called on instances of either the Parent
or Child
class, virtual functions were about 7.5x slower than non-virtual functions. That’s a major gap and certainly a good reason to allow C# programmers to opt-out of them.
On the flip side, virtual function calls in this test are taking 0.00000235 milliseconds each. You’d need to make about 50,000 such calls to eat up one millisecond of CPU time. That’s a lot of calls, but not entirely out of the question for a complex game. If virtual functions were the only option—as in AS3 and Java—it’s possible that their overhead could appear during profiling as a significant portion of a 16 millisecond frame (60 FPS).
Performance problems might crop up quite a bit quicker than 50,000 function calls per frame. The CPU used for the test is actually quite fast, especially compared to many CPUs found in mobile devices. In rough terms, the test’s CPU is about twice as fast as a mobile CPU. That means you’d only need 25,000 virtual function calls to eat up a millisecond and that’s much more likely.
It’s definitely fine to make a few function calls virtual as it makes you a more productive programmer. However, be wary if you’re going to make thousands of virtual function calls per frame- you might end up with a performance problem.
In the end, should we be glad that functions are non-virtual by default in C#? Let me know your opinion in the comments.
#1 by Mars on May 18th, 2015 ·
Virtual functions is useful for override , and is friendly to oop or our program
#2 by devboy on October 3rd, 2015 ·
Did you actually try to compile this with IL2CPP? It always produced garbage c++ for me in the past.
#3 by jackson on October 3rd, 2015 ·
No. At the time IL2CPP was extremely unstable. It’s a lot better since then, so perhaps it’s worth another shot.
#4 by DungDajHjep on August 21st, 2016 ·
Thank for test !
#5 by Moses on April 3rd, 2018 ·
I recommend you run this test again after explicitly disabling function optimization and inlining. C sharp runs optimizations when JIT compiling the code, so it will optimize away those non-virtual empty methods. This link should explain how to disable the optimization and inlining.
https://stackoverflow.com/questions/38632939/disable-compiler-optimisation-for-a-specific-function-or-block-of-code-c
#6 by jackson on April 3rd, 2018 ·
That may well be the case with Mono as it’s definitely the case with IL2CPP. Consider this test C#:
IL2CPP outputs this C++:
And that compiles to this ARM assembly:
So the virtual function version is still slower, but the test doesn’t hold up very well when one of the functions is completely removed by the optimizer as you said would happen with the Mono JIT. A new performance test should be created to encourage the compiler to not remove the non-virtual function call, but in the meantime I’ve covered the IL2CPP output in-depth here.
#7 by Felipe Machado on February 12th, 2019 ·
One of the biggest hits with virtual methods is that they can’t be inlined. So, with such small functions we may be experiencing the benefits of the ‘inline’ optimization in the non-virtual method and not seeing the actual cost of the virtual table lookup, which is a O(1) operation (it will be much faster for ‘polymorphic behavior’ than a ‘switch’ statement which could not be properly optimized into a jump table, for example). It would be nice to extend this test for a ‘direct’ method that couldn’t be inlined vs the virtual method.
#8 by jackson on February 12th, 2019 ·
Excellent point and a great idea for a follow-up article!
#9 by JackMariani on October 21st, 2019 ·
This might be late, and maybe you’ve already covered it on another post.
Anyway, I remember I read that sealed class offer better performance on virtual functions (see also: http://codebetter.com/patricksmacchia/2008/01/05/rambling-on-the-sealed-keyword/) so I tested the child with a sealed class and I got better results.
This is the class I used in the test. It might be the case the sealed class improve the performance of virtual functions.
#10 by jackson on October 21st, 2019 ·
As the link mentions, the performance boost you’re seeing is due to the virtual call being turned into a non-virtual call.
sealed
is a great keyword to give the compiler a hint that it should do this optimization.#11 by Geoff on February 15th, 2023 ·
I recently saw an article by a lead developer in the C# team lamenting that they hadn’t made sealed classes the default in the original design.
To late now, obviously, with so much legacy code around.
But they are recommending that you manually designate classes as sealed whenever possible, to enjoy the performance benefits.