Oh, performance testing! Testing is a rather difficult topic, it is very difficult to make it completely correct. It is believed that this is a good task for a beginner, but in fact the architect should have written the right performance tests.
Let's take a look at what we want to test.
First, running the method once is not a good thing. The execution of the method is influenced by hundreds of random parameters: for example, a random surge in the activity of the antivirus or a page of the code that is forced out to disk. Therefore, it is necessary to run the method many times: the results of a single run are statistically irrelevant. (Regarding how many times you need to run a test: do not take a number from the ceiling, but ask a mathematics colleague!)
Further, the test should be carried out with the same parameters ! Otherwise, our averaging does not make sense: we average incomparable values. If the function of one code branch is fast and the other is slow, it is meaningless to average the run time over them: you need to have two tests that test different branches.
Thus, we have to execute the same code many times: call a function with the same parameters. This means that our function call wraps around perfectly in Action
, and you can use the same method to test different functions:
class PerformanceTester { const int repetitions = 1000; // может быть, нужна внешняя параметризация static public TimeSpan ComputeAverageExecutionTime(Action a) { var executionTicks = new List<long>(); for (int i = 0; i < repetitions; i++) executionTicks.Add(MeasureTime(a)); double averageTicks = executionTicks.Average(); return new TimeSpan((long)averageTicks); } static private long MeasureTime(Action a) // in ticks { var stopwatch = new Stopwatch(); stopwatch.Start(); a(); stopwatch.Stop(); return stopwatch.ElapsedTicks; } }
Use this:
void f() { /* ... */ } void g(int arg) { /* ... */ } void h(ClassArg arg1, int arg2) { /* ... */ } ... var r1 = PerformanceTester.ComputeAverageExecutionTime(f); var r2 = PerformanceTester.ComputeAverageExecutionTime(() => g(1)); ClassArg arg1 = new ClassArg(); // не включаем конструктор в лямбду var r3 = PerformanceTester.ComputeAverageExecutionTime(() => h(arg1, 0));
But this is not the final decision. It has flaws that we will try to close.
First, JIT. The first run of the function will be slower due to the fact that at this moment it will be compiled by just-in-time-compiler. Plus the necessary parts of the code will be loaded into memory. So you need to include the "warming up code" in the test:
static public TimeSpan ComputeAverageExecutionTime(Action a) { // разогреваем код: a(); var executionTicks = new List<long>(); for (int i = 0; i < repetitions; i++) executionTicks.Add(MeasureTime(a)); double averageTicks = executionTicks.Average(); return new TimeSpan((long)averageTicks); }
Secondly, among a large number of runs, there will still be random "emissions" up and down. To level them, one must know the standard deviation σ, and reject results that differ from the estimated average by more than 3σ. But this requires advanced mathematics, and we can simply throw out the largest and smallest values; this is also a pretty good heuristic.
var maxVal = executionTicks.Max(); if (executionTicks.Count(v => v == maxVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v => v == maxVal); var minVal = executionTicks.Min(); if (executionTicks.Count(v => v == minVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v => v == minVal); // здесь осталось не менее [[1000 * 2/3] * 2/3] = 444 элементов, // значит, список не пуст, и исключение не выбросится double averageTicks = executionTicks.Average();
Third, let's see what we measure. In addition to the function itself, we measure the time of its indirect call , via the lambda and the delegate. For most functions, this is irrelevant: the run time of a function is usually much longer than the call time for a delegate, so this subtlety can be neglected. Another thing, if your function is very small, for this you will have to write test code manually.
For example, such:
class PerformanceTester { static public TimeSpan ComputeExecutionTimeForSmallFunctions( Action execute1000times, Action execute1time) { // разогреваемся execute1time(); var ticks = MeasureTime(execute1000times); return new TimeSpan((long)(ticks / 1000.0)); } // остальные функции }
We use this:
void q() { /* что-то очень быстрое */ } var r4 = PerformanceTester.ComputeExecutionTimeForSmallFunctions( () => { for (int i = 0; i < 1000; i++) q(); } q);
Disadvantages: the indirect call of the delegate is still included in the measurement, but now the delegate is not called 1000, but only 1 time, and introduces only 1/1000 distortion. Fortunately, for a small function, you can safely raise the constant 1000 to large values, for example, to 1000000. At the same time, the results of random large deviations from the average with which we struggled for long functions by discarding the minimum and maximum values are leveled. In addition, the cycle control time is added to the execution time of the function.
Fourth, do not forget that performance is highly dependent on the compiler settings. Therefore, never test the performance of a code compiled in DEBUG mode. In addition, the mileage in Visual Studio slows down the speed of the code approximately twice (!), Since the debugger specifically “asks” the JIT compiler to optimize less. Therefore, after debugging, run the performance tests only outside Visual Studio, and only compiled in RELEASE mode.
Fifth, do not forget about the presence of a pretty smart optimizer! It has the right to call a function call, and if you ignore the result, throw out the entire calculation! Therefore, in the case of ComputeExecutionTimeForSmallFunctions
return value should somehow be used, at least stored in an array, added to a battery or something else.
void MyMethod(Action a)
method, to which you can send different lambdas:MyMethod(d1)
,MyMethod(() => d2(arg))
,MyMethod(() => d3(arg1, arg2))
, etc. - VladD