MQL's OOP notes: Self-made profiler on static and automatic objects

25 October 2016, 22:01
Stanislav Korotky
MQL code performance is important in many aspects. For example, if your expert adviser is well optimized you could pay much less for it running in the cloud. Or you can eliminate a situation when your  terminal becomes unresponsive for a while while it calculates a heavy indicator. Of course, there can be many different ways of optimization (such as code refactoring, proper algorithm selection, caching, and a lot of others deserving thorough consideration, but implying a mass of text far larger than a single blog post can comprise). But the first step is always to analyze the code performance and find bottlenecks. MetaTrader provides a built-in tool for timing analysis of functions and even single lines of code - the profiler.

Unfortunately, the built-in profiler works only in online charts, where inefficient parts of code are not so easy to spot. It would be much more convenient to profile code in the tester (during single tests or even batch optimization of an expert adviser). For that purpose we can implement a profiler ourselves as a set of classes. It will be simple yet sufficient enough to perform main profiler tasks.

The header file Profiler.mqh is attached below. It contains 3 classes: Profiler, ProfilerLocator, and ProfilerObject.

  • Profiler - implements the main functionality;
  • ProfilerLocator - is intended for pointing program contexts or parts (such as functions or sequences of code lines), where we want to measure performance;
  • ProfilerObject - is a helper class, which allows for automating starts/stops of profiling process in accordance to its objects lifetime cycles; for example, you can declare such an object in a function (specifically, create an automatic instance on the stack), and then - no matter how many exit points the function has - every time control flow leaves the scope of the function, the object will be destroyed, which in turn will stop profiling (until the next time the function is entered again, which in turn will start profiling automatically again).

The comments in the file should make the code almost self-explainatory. And introductory information is given here in the text.

The Profiler object is already declared in the header. By default it's initialized with 100 points for measurements, that is one can evaluate duration of execution of 100 code fragments. Every point is basically backed with an element in internal arrays which store time lapse and count of times of code execution. If you need more points, adjust the initialization: the required number of measurement points is passed as the parameter of the Profiler constructor.

Profiling process is controlled by invocation of the profiler methods at specific points in your code. Here are some examples of the profiler usage.

The simpliest, but slowest (and hence not recommended) method is:

int pid = profiler.StartProfiler("UniqueNameOfTheFragment");
... // code for profiling

The fragment above is identified by a unique name, specified in the StartProfiler method. There are more efficient forms of the method StartProfiler, for example the one which identifies code fragments by integer numbers. For this variant to use, one could first register all code fragments in the profiler (for example, in OnInit):

profiler.SetName(1, "First");
profiler.SetName(2, "Second");

Then you can start and stop profiling by the following calls:

... // code fragment 1
... // code fragment 2

As a convenient way of defining profiling points one may use ProfilerLocator objects inline - just before corresponding code fragments: 

static ProfilerLocator locator3("CalculateProfit");
... // code

The fragment above is identified by the locator object, which generates a unique integer identifier internally. Please note that the locator object is declared static, because it should be created only once. This guarantees that every execution of the code fragment will use the same profiling element accumulating statistics. If the object would not be static, every single pass would generate (reserve) new measurement point, and everyone would have the same name "CalculateProfit". 

Finally, one of the most popular variants of profiling - function profiling - can be achieved by placing the following lines at the beginning of a function: 

static ProfilerLocator locator(__FUNCTION__);
ProfilerObject obj(locator);

Automatic creation of the ProfilerObject object starts profiling of the entire function scope automatically, and its deletion upon function termination stops profiling automatically as well. A shorthand macro _PROFILEFUNC_ is defined for this in the header file.

After all profiling points are set by one of the methods of the profiler, you can access gathered statistics at the end of MQL program execution (for example, in OnTester). The function Profiler::GetStats(id) returns a string with a number of times and total time of execution of the fragment with the given identifier id. You may also output all statistics in the log by Profiler::PrintAllStats().

Here is an example.

#include <profiler.mqh>

#define CALC_PROFILE_ID 10


void subfunc()
  for(int i = 0; i < 1000; i++)
    for(int j = 0; j < 10000; j++)
      MathSqrt(MathPow(i, 3));


void OnStart()
  profiler.SetName(CALC_PROFILE_ID, "calculation");

This script will produce something like this:

Profiler stats: name count seconds milliseconds     %% Comments
        profilertest     1       1         1607 100.00 WARNING! Keeps running
             subfunc     1       1         1107  68.89
         calculation  1000       0          858  53.39


Profiler.mqh 15 kb
Share it with friends: