Micro benchmarks and hot paths
I’m doing some work to refactor a complex piece of code, and I have a piece of memory that is allocated, then accessed via a struct pointer. This piece of code gets called a lot, so I wondered about the tradeoff of holding a pointer that is already casted to the right struct pointer vs the cost of another pointer in the object.
Note, those are objects that are created a lot, and used a lot. Those are not the kind of things that you would usually need to worry about. Because I wasn’t sure, I decided to test this out. And I used BenchmarkDotNet to do so:
public struct FooHeader { public long PageNumber; public int Size; } [BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)] public unsafe class ToCastOrNotToCast { byte* p; FooHeader* h; public ToCastOrNotToCast() { p = (byte*)Marshal.AllocHGlobal(1024); h = (FooHeader*)p; } [Benchmark] public void NoCast() { h->PageNumber++; } [Benchmark] public void Cast() { ((FooHeader*)p)->PageNumber++; } [Benchmark] public void DirectCastArray() { ((long*)p)[0]++; } [Benchmark] public void DirectCastPtr() { (*(long*)p)++; } }
The last two tests are pretty much just to have something to compare to, because if needed, I would just calculate memory offsets manually, but I doubt that those would be needed.
The one downside of BenchmarkDotNet is that it takes a very long time to actually run those tests. I’ll save you the suspense, here are the results:
I was expected the NoCast method to be faster, to be honest. But the Cast method is consistently (very slightly) the fastest one. Which is surprising.
Here is the generated IL:
And the differences in assembly code are:
Note that I’m not 100% sure about the assembly code. I got it from the disassembly windows in VS, and it is possible that it changed what is actually going on.
So I’m not really sure why this would be difference, and it is really is a small difference. But it is there.
Comments
I think that the fact that one is a pointer to a byte and the other is a pointer to a structure is only relevant to the compiler for syntax checking and optimizing memory layout. Once you get to the assembly code this has all been resolved and both are just values read from a memory address and added to a register.
Try swapping the position of the byte* and FooHeader* in the struct... Might be that you're getting unlucky and the FooHeader* is on the different cache line than the rest of the struct for some of the trials.
The difference is within one StdDev so it's not really there.
If this was compiled by a C compiler any difference would be a compiler bug. But with the bad .NET JITs I would believe a difference if one was found.
Glad you like BenchmarkDotNet, I think it's a great library (although I'm biased as I contribute to it)
You probably know this, but it's deliberately taking a long time. It tries very hard to get accurate results by measuring lots of loops, over multiple runs and in different processes. You can control some of this, but the defaults are chosen to give accurate measurements.
@Matt accuracy always wins over speed, being slow is not a bad thing in this case ;)
Comment preview