A performance profile mystery: The cost of Stopwatch
Measuring the length of time that a particular piece of code takes is a surprising challenging task. There are two aspects to this, the first is how do you ensure that the cost of getting the start and end times won’t interfere with the work you are doing. The second is how to actually get the time (potentially many times a second) in as efficient way as possible.
To give some context, Andrey Akinshin does a great overview of how the Stopwatch class works in C#. On Linux, that is basically calling to the clock_gettime system call, except that this is not a system call. That is actually a piece of code that the Kernel sticks inside your process that will then integrate with other aspects of the Kernel to optimize this. The idea is that this system call is so frequent that you cannot pay the cost of the Kernel mode transition. There is a good coverage of this here.
In short, that is a very well-known problem and quite a lot of brainpower has been dedicated to solving it. And then we reached this situation:
What you are seeing here is us testing the indexing process of RavenDB under the profiler. This is indexing roughly 100M documents, and according to the profiler, we are spending 15% of our time gathering metrics?
The StatsScope.Start() method simply calls Stopwatch.Start(), so we are basically looking at a profiler output that says that Stopwatch is accounting for 15% of our runtime?
Sorry, I don’t believe that. I mean, it is possible, but it seems far-fetched.
In order to test this, I wrote a very simple program, which will generate 100K integers and test whether they are prime or not. I’m doing that to test compute-bound work, basically, and testing calling Start() and Stop() either across the whole loop or in each iteration.
I run that a few times and I’m getting:
- Windows: 311 ms with Stopwatch per iteration and 312 ms without
- Linux: 450 ms with Stopwatch per iteration and 455 ms without
On Linux, there is about 5ms overhead if we use a per iteration stopwatch, on Windows, it is either the same cost or slightly cheaper with per iteration stopwatch.
Here is the profiler output on Windows:
And on Linux:
Now, that is what happens when we are doing a significant amount of work, what happens if the amount of work is negligible? I made the IsPrime() method very cheap, and I got:
So that is a good indication that this isn’t free, but still…
Comparing the costs, it is utterly ridiculous that the profiler says that so much time is spent in those methods.
Another aspect here may be the issue of the profiler impact itself. There are differences between using Tracing and Sampling methods, for example.
I don’t have an answer, just a lot of very curious questions.