C# Performance: Properties vs. Fields vs. Locals
C# has properties similar to AS3’s get
and set
functions. These functions can even be auto-generated for you, which is very convenient. However, the auto-generated versions don’t expose the so-called “backing field” that the property gets and sets. This brings up a question: is there a performance penalty to using an auto-generated property rather than manually implementing the property so we can directly access the backing field? The get
and set
blocks are like functions, so are we paying function call overhead for them every time we access our own private pseudo-variables? Finally, could we do even better by skipping fields altogether and working on local variables instead? Today’s article puts all three approaches to the test by analyzing the bytecode that’s generated and the performance within a Unity test environment. Read on to see which way is fastest!
First let’s start with a snippet of code to see what kind of code is being generated by the compiler when we use automatic properties. Those are simply properties where we don’t define the get
and set
blocks ourselves. Here’s how it looks:
public class Test { public int IntProperty { get; private set; } public Test() { IntProperty = 5; } }
Now let’s take a look at the bytecode that’s generated for both the property and the call to set its value. As in the article where I analyzed the bytecode that is automatically generated by C# events, I’ll look at a few common compilers. Unlike that article, I’m using dotPeek by JetBrains to do the decompliation.
First, let’s look at the latest Mono 3.12 compiler that MonoDevelop/Xamarin Studio uses:
[CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int \u003CIntProperty\u003Ek__BackingField; public int IntProperty { get { return this.\u003CIntProperty\u003Ek__BackingField; } private set { this.\u003CIntProperty\u003Ek__BackingField = value; } } public Test() { base.\u002Ector(); this.IntProperty = 5; }
Here we see that IntProperty
has been expanded in just the way we’d expect. A backing field has been declared private
with a couple of annotations. The get
just returns this backing field and the set
just sets it to whatever is passed as value
. The line that sets it to 5
just calls the set
block. It does not directly access the backing field.
Now let’s look at how Microsoft Visual Studio 2013 compiles the same code:
[CompilerGenerated] private int \u003CIntProperty\u003Ek__BackingField; public int IntProperty { get { return this.\u003CIntProperty\u003Ek__BackingField; } private set { this.\u003CIntProperty\u003Ek__BackingField = value; } } public Test() { base.\u002Ector(); this.IntProperty = 5; }
Visual Studio has compiled this code almost exactly like Mono has. The only difference is that Mono also included a [DebuggerBrowsable]
annotation on the backing field where Visual Studio did not.
Finally, let’s see how Unity 4.6.2 compiles the code by decompiling Library/ScriptAssemblies/Assembly-CSharp.dll
:
[CompilerGenerated] private int \u003CIntProperty\u003Ek__BackingField; public int IntProperty { get { return this.\u003CIntProperty\u003Ek__BackingField; } private set { this.\u003CIntProperty\u003Ek__BackingField = value; } } public Test() { base.\u002Ector(); this.IntProperty = 5; }
Unity has compiled the code exactly the same as Visual Studio. All three of them are essentially identical, save for Mono’s additional annotation on the backing field. That annotation shouldn’t have any performance impact at all. For the purposes of this test, the results should apply to all three commonly-used compilers.
Now let’s take a look at a quick test that sums the first billion integers. There are three test cases. The first retrieves the current sum from an auto-generated property, adds to it, then stores it back to the auto-generated property. The second retrieves from and stores to a backing field without using a property. The third uses a local variable and then stores to a backing field once the final sum has been computed.
Here’s how the test looks. If you want to recreate it, just create a new project and attach this script to your main camera.
using System.Diagnostics; using UnityEngine; using UnityEngine.UI; public class TestScript : MonoBehaviour { private const long NumValues = 1000000000; public long SumAuto { get; private set; } private long sum; public long SumManual { get { return sum; } set { sum = value; } } private string report; void Start() { var stopwatch = new Stopwatch(); stopwatch.Reset(); stopwatch.Start(); for (long i = 0; i < NumValues; ++i) { SumAuto += i; } var propertyTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (long i = 0; i < NumValues; ++i) { sum += i; } var fieldTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); long localSum = 0; for (long i = 0; i < NumValues; ++i) { localSum += i; } sum = localSum; var localTime = stopwatch.ElapsedMilliseconds; report = "Test,Time\n" + "Property," + propertyTime + "\n" + "Field," + fieldTime + "\n" + "Local," + localTime + "\n"; } void OnGUI() { var drawRect = new Rect(0, 0, Screen.width, Screen.height); GUI.TextArea(drawRect, report); } }
I tested this app using the following environment:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.2
- Unity 4.6.2, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Time |
---|---|
Property | 1950 |
Field | 1933 |
Local | 628 |
The data immediately makes it obvious that directly using the property, even with the auto-generated get
and set
blocks, is no more expensive than directly using a field. There is no function call overhead, despite the function-like behavior that those blocks have.
But that’s not to say that properties and fields are fast. They’re actually about 3x slower than using a local variable! In cases like these where you are going to do a lot of operations on a field, it’s best for performance if you cache it locally.
Keep in mind though that none of these operations are very expensive on their own. There were, after all, a billion iterations of each loop. That makes the property access just .000001950 milliseconds per operation. You’d need to do a million of them to use about 2 milliseconds. That’s a lot for a single frame though, so much lower numbers may adversely affect performance. It’s more of the kind of thing that won’t show up on a profiler because the field/property accesses are spread throughout the code and add up to a performance problem. Luckily, you can avoid the problem by caching to local variables when justified.
Spot a bug? Have a question or suggestion? Post a comment!
#1 by henke37 on February 16th, 2015 ·
I bet that the JIT simply has a special case for trivial accessor functions. It’s obvious low hanging fruit.
#2 by jackson on February 16th, 2015 ·
I suspect the same.
#3 by dknox on February 16th, 2015 ·
Is SumManual used anywhere? Also, when you say “avoid the problem by caching to local variables when justified” – when would/wouldn’t you want to use the local approach? Is it simply more verbose and that is why it may not be justifiable?
#4 by jackson on February 16th, 2015 ·
No,
SumManual
was just included to demonstrate how properties could still be used the “manual” way. The test has no need for it, but it’s part of the class’ public API.As for justification, that’ll be up to each developer. If performance isn’t a concern, as it often isn’t, then there’s no need to cache as a local variable. It would simply increase the amount of code you need to write and maintain. Another place where it’s not justified is if you’re only going to perform one operation on the variable, or just a few. For example, this would actually perform worse than just using the property directly:
It’s also more to write and less clear to read.
#5 by Oleksiy on July 16th, 2015 ·
Thanks, good article
#6 by hamfburgrer on January 10th, 2018 ·
[deleted]
#7 by hamfburgrer on January 10th, 2018 ·
[deleted]
#8 by Sadegh on February 1st, 2020 ·
I’m making games for mobile with Unity and I always have performance problems in different parts of the game that needs to be fixed. Articles like this will be very helpful.
Tnx Jackson.
#9 by taffer on July 3rd, 2020 ·
> But that’s not to say that properties and fields are fast. They’re actually about 3x slower than using a local variable!
Well, for classes, yes. But try to repeat the test with a struct where there is no need to dereference the self instance to access a field.