Reputation: 206
I have an immutable struct I'm using for a 3D vector. I am aware that basic getter properties are (supposed to be) inlined and therefore should perform identical to fields assuming you are in release configuration and running outside of VS without any influence from debugging or JIT suppression. However, that's not the behavior I'm seeing and I'm trying to figure out why. (By the way, I've read every other post about this on SO that I can find to no avail).
Setup: VS2019 v16.8.4, using .NET 5.0 & C#9. Release configuration, with Optimization enabled in all projects.
First, the relevant piece of my vector class using public fields (for comparison):
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Vector3
{
public readonly double x;
public readonly double y;
public readonly double z;
...
}
and the Console app code to test the time to access a field from 1 million vectors. (I know I can do things to make the benchmark code better, such as increasing thread priority or warming up the function, but the key here is the difference between the performance I see when using fields vs. properties--which is consistent):
static void Main(string[] args)
{
const int sampleSize = 1000000;
var vectors = new Vector3[sampleSize];
// Fill with random values.
var rand = new Random();
for (var i = 0; i < sampleSize; i++)
{
vectors[i] = new Vector3(rand.NextDouble(),
rand.NextDouble(),
rand.NextDouble());
}
double val;
var sw = Stopwatch.StartNew();
for (var i = 0; i < sampleSize; i++)
val = vectors[i].x; //Access the field as a test.
sw.Stop();
Console.WriteLine($"Accessing the fields 1M times took {sw.ElapsedTicks} ticks.");
When I build this code in release configuration, start the command prompt, and run the executable, it takes around 3100 to 3400 ticks to access the x field a million times.
If I change this code to use a simple getter instead:
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Vector3
{
public double X { get; }
public double Y { get; }
public double Z { get; }
...
}
and the only change to Main()
is this:
val = vectors[i].X; //Now access via a property instead of the field.
(that is, accessing via the auto-prop instead of the field) then when I build this in release config and run the executable several times in the command prompt, I see times in the 15,000 tick range, or about 5 times slower!
Again, I have confirmed for both tests that I am compiling in release configuration, with optimization checked in all of my projects, and I am running the executable in the command prompt outside of Studio (so the debugger can't possibly be attached).
What am I missing? Why is the property access taking 5 times longer?
Upvotes: 2
Views: 121
Reputation: 19956
The property access might be taking longer because the property access is not inlined. You can play around with your code at SharpLab.io - where you can see that no inlining happens.
Also, make sure you do not measure the first run of your executable. Upon first execution there is no jitted code, hence no inlined code. Do you see the same numbers on each run?
Sure, the compile may inline your properties. In your code base - in some specific context - the compiler perhaps decides not to inline your properties.
Upvotes: 0
Reputation: 81503
I am not seeing your results
The results are all within a typical margin or error given my CPU and environment
Note, you should always use a benchmarking tool for these sort of tests, there are lot of things that go wrong otherwise. In this case I am using BenchmarkDotnet
Config
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.746 (2004/?/20H1)
Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.102
[Host] : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
Job=.NET Core 5.0 Runtime=.NET Core 5.0
Results
Method | Mean | Error | StdDev |
---|---|---|---|
Field | 1.535 ms | 0.0307 ms | 0.0605 ms |
Prop | 1.512 ms | 0.0204 ms | 0.0171 ms |
PropInline | 1.567 ms | 0.0295 ms | 0.0404 ms |
Given
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Struct1
{
public readonly double x;
public readonly double y;
public readonly double z;
public Struct1(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Struct2
{
public double x { get; }
public double y{ get; }
public double z{ get; }
public Struct2(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Struct3
{
public double x
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
public double y
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
public double z
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
public Struct3(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
Test Code
[SimpleJob(RuntimeMoniker.NetCoreApp50 )]
public class Test
{
private Struct1[] _vector1s;
private Struct2[] _vector2s;
private Struct3[] _vector3s;
[GlobalSetup]
public void Setup()
{
const int sampleSize = 1000000;
_vector1s = new Struct1[sampleSize];
_vector2s = new Struct2[sampleSize];
_vector3s = new Struct3[sampleSize];
// Fill with random values.
var rand = new Random();
for (var i = 0; i < sampleSize; i++)
{
var x = rand.NextDouble();
var y = rand.NextDouble();
var z = rand.NextDouble();
_vector1s[i] = new Struct1(x,y,z);
_vector2s[i] = new Struct2(x,y,z);
_vector3s[i] = new Struct3(x,y,z);
}
}
[Benchmark]
public double Field()
{
double val =0;
unchecked
{
for (var i = 0; i < 1000000; i++)
val += _vector1s[i].x;
}
return val;
}
[Benchmark]
public double Prop()
{
double val =0;
unchecked
{
for (var i = 0; i < 1000000; i++)
val += _vector2s[i].x;
}
return val;
}
[Benchmark]
public double PropInline()
{
double val =0;
unchecked
{
for (var i = 0; i < 1000000; i++)
val += _vector3s[i].x;
}
return val;
}
}
Upvotes: 3