📜 ⬆️ ⬇️

Under the hood at Stopwatch

Introduction


Very often, we developers need to measure the execution time of their (and not only their) code. When I first started programming, I used the DateTime structure for this purpose. Time passed, and I learned about the class Stopwatch and began to actively use it. I think you had a similar situation. It’s not that I hadn’t previously wondered how Stopwatch works, but at that time I’m aware that it allows me to measure elapsed time more accurately than DateTime was enough for me. The time has come to explain to ourselves, as well as to readers, how the Stopwatch class actually works, as well as to find out its advantages and disadvantages compared to using DateTime.

Using DateTime


Using the DateTime structure to measure the execution time of a code is quite simple:

var before = DateTime.Now; SomeOperation(); var spendTime = DateTime.Now - before; 

Property DateTime.Now - returns the local current date and time. Instead of the DateTime.Now property, you can use the DateTime.UtcNow property — which returns the current date and time, but instead of the local time zone, it represents them as Utc time, that is, as coordinated universal time.

 var before = DateTime.UtcNow; SomeOperation(); var spendTime = DateTime.UtcNow - before; 

A few words about the datetime structure


Perhaps few thought about what a DateTime structure is. The value of the DateTime structure is measured in 100-nanosecond units, called ticks, and the exact date is represented by the number of ticks elapsed since 00:00 January 1, 0001 AD.
')
For example, the number 628539264000000000 represents October 6, 1992 00:00:00.

The DateTime structure contains a single field that contains the number of cycles that have passed:

 private UInt64 dateData; 

It should also be said that starting with .NET 2.0, the 2 high-order bits of this field indicate the type of DateTime: Unspecfied — not set, Utc — coordinated time, Local — local time, and the remaining 62 bits — the number of ticks. We can easily query these two bits using the Kind property.

What is wrong with using datetime?


Using the DateTime.Now property to measure time intervals is not a good idea, and here's why:

DateTime.Now
 public static DateTime Now { get { DateTime utc = DateTime.UtcNow; Boolean isAmbiguousLocalDst = false; Int64 offset = TimeZoneInfo.GetDateTimeNowUtcOffsetFromUtc(utc, out isAmbiguousLocalDst).Ticks; long tick = utc.Ticks + offset; if (tick > DateTime.MaxTicks) { return new DateTime(DateTime.MaxTicks, DateTimeKind.Local); } if (tick < DateTime.MinTicks) { return new DateTime(DateTime.MinTicks, DateTimeKind.Local); } return new DateTime(tick, DateTimeKind.Local, isAmbiguousLocalDst); } } 


The calculation of the DateTime.Now property is based on DateTime.UtcNow, that is, the coordinated time is first calculated and then the time zone offset is applied to it.

That is why using the DateTime.UtcNow property will be more correct, it is calculated much faster:

DateTime.UtcNow
 public static DateTime UtcNow { get { long ticks = 0; ticks = GetSystemTimeAsFileTime(); return new DateTime(((UInt64)(ticks + FileTimeOffset)) | KindUtc); } } 


The problem with using DateTime.Now or DateTime.UtcNow is that their accuracy is fixed. As stated above

1 tick = 100 nanoseconds = 0.1 microseconds = 0.0001 milliseconds = 0.0000001 seconds

accordingly, it is simply impossible to measure the time interval whose length is less than the length of one measure. Of course, it is unlikely that you will need it, but you need to know it.

Using the Stopwatch class


The Stopwatch class appeared in .NET 2.0 and has not undergone any changes since. It provides a set of methods and tools that can be used to accurately measure elapsed time.

The public API of the Stopwatch class looks like this:

Properties
  1. Elapsed - returns the total elapsed time;
  2. ElapsedMilliseconds - returns the total elapsed time in milliseconds;
  3. ElapsedTicks - returns the total elapsed time in timer ticks;
  4. IsRunning - returns a value indicating whether the Stopwatch timer is running.

Methods
  1. Reset - stops the measurement of the time interval and resets the elapsed time;
  2. Restart - stops the measurement of the time interval, resets the elapsed time and starts measuring the elapsed time;
  3. Start - starts or continues measurement of the elapsed time for the interval;
  4. StartNew - initializes a new instance of Stopwatch, sets the elapsed time property to zero and starts the elapsed time measurement;
  5. Stop - stops the measurement of the elapsed time for the interval.

Fields
  1. Frequency - returns the frequency of the timer, as the number of ticks per second;
  2. IsHighResolution - indicates whether the timer depends on the high-resolution performance counter.

Code that uses the Stopwatch class to measure the execution time of the SomeOperation method might look like this:

 var sw = new Stopwatch(); sw.Start(); SomeOperation(); sw.Stop(); 

The first two lines can be written more succinctly:

 var sw = Stopwatch.StartNew(); SomeOperation(); sw.Stop(); 

Stopwatch implementation


The Stopwatch class is based on HPET (High Precision Event Timer, High Precision Event Timer). This timer was introduced by Microsoft to once and for all put an end to the problems of measuring time. The frequency of this timer (minimum 10 MHz) does not change while the system is running. For each system, Windows itself determines with what devices to implement this timer.

The Stopwatch class contains the following fields:

 private const long TicksPerMillisecond = 10000; private const long TicksPerSecond = TicksPerMillisecond * 1000; private bool isRunning; private long startTimeStamp; private long elapsed; private static readonly double tickFrequency; 

TicksPerMillisecond - determines the number of DateTime cycles in 1 millisecond;
TicksPerSecond - determines the number of datetime times in 1 second;

isRunning - determines whether the current instance is running (if the Start method was called);
startTimeStamp - the number of cycles at the time of launch;
elapsed - the total number of cycles spent;

tickFrequency - simplifies the translation of Stopwatch clocks into DateTime clocks.

The static constructor checks for the presence of the HPET timer and, if it is missing, the Stopwatch frequency is set equal to the DateTime frequency.

Static constructor Stopwatch
 static Stopwatch() { bool succeeded = SafeNativeMethods.QueryPerformanceFrequency(out Frequency); if(!succeeded) { IsHighResolution = false; Frequency = TicksPerSecond; tickFrequency = 1; } else { IsHighResolution = true; tickFrequency = TicksPerSecond; tickFrequency /= Frequency; } } 


The main scenario of the work of this class was shown above: a call to the Start method, a method whose time must be measured, and then a call to the Stop method.

The implementation of the Start method is very simple - it remembers the initial number of measures:

Start
 public void Start() { if (!isRunning) { startTimeStamp = GetTimestamp(); isRunning = true; } } 


It should be said that calling the Start method on an already measuring instance does not lead to anything.

The Stop method is just the same:

Stop
 public void Stop() { if (isRunning) { long endTimeStamp = GetTimestamp(); long elapsedThisPeriod = endTimeStamp - startTimeStamp; elapsed += elapsedThisPeriod; isRunning = false; if (elapsed < 0) { // When measuring small time periods the StopWatch.Elapsed* // properties can return negative values. This is due to // bugs in the basic input/output system (BIOS) or the hardware // abstraction layer (HAL) on machines with variable-speed CPUs // (eg Intel SpeedStep). elapsed = 0; } } } 


Calling the Stop method on a stopped instance also results in nothing.

Both methods use the GetTimestamp () call, which returns the number of ticks at the time of the call:

Gettimestamp
 public static long GetTimestamp() { if (IsHighResolution) { long timestamp = 0; SafeNativeMethods.QueryPerformanceCounter(out timestamp); return timestamp; } else { return DateTime.UtcNow.Ticks; } } 


With HPET (High Precision Event Timer), the Stopwatch beats are different from the DateTime beats.

Following code

 Console.WriteLine(Stopwatch.GetTimestamp()); Console.WriteLine(DateTime.UtcNow.Ticks); 

on my computer displays

 5201678165559 635382513439102209 

Using the Stopwatch measures to create a DateTime or TimeSpan is incorrect. Record

 var time = new TimeSpan(sw.ElaspedTicks); 

for obvious reasons, will lead to incorrect results.

To get the DateTime clock cycles, and not Stopwatch, you need to use the Elapsed and ElapsedMilliseconds properties or do the conversion manually. To convert Stopwatch clocks to DateTime clocks in a class, use the following method:

GetElapsedDateTimeTicks
 private long GetElapsedDateTimeTicks() { long rawTicks = GetRawElapsedTicks();// get Stopwatch ticks if (IsHighResolution) { // convert high resolution perf counter to DateTime ticks double dticks = rawTicks; dticks *= tickFrequency; return unchecked((long)dticks); } else { return rawTicks; } } 


The property code looks as expected:

Elapsed, ElapsedMilliseconds
 public TimeSpan Elapsed { get { return new TimeSpan(GetElapsedDateTimeTicks()); } } public long ElapsedMilliseconds { get { return GetElapsedDateTimeTicks() / TicksPerMillisecond; } } 


What is wrong with using Stopwatch?


A note to this class with MSDN says: on a multiprocessor computer, it does not matter which of the processors is running. However, due to errors in the BIOS or the abstracted hardware layer (HAL), you can get different results of calculating the time on different processors.

In order to avoid this, the if (elapsed <0) condition is in the Stop method.

I found a lot of articles whose authors are faced with problems due to incorrect work of HPET.

In the absence of HPET, Stopwatch uses DateTime bars, so its advantage over explicitly using DateTime is lost. In addition, you need to take into account the time for method calls and checks performed by Stopwatch, especially if it happens in a loop.

Stopwatch in mono


I wondered how the Stopwatch class was implemented in mono, since it’s not necessary to rely on Windows native functions for working with HPET.

 public static readonly long Frequency = 10000000; public static readonly bool IsHighResolution = true; 

Stopwatch in mono always uses DateTime clocks, and therefore it has no advantages over explicitly using DateTime, except that the code is more readable.

Environment.TickCount


It should also be said about the Environment.TickCount property, which returns the time elapsed since the system was booted (in milliseconds).

The value of this property is retrieved from the system timer and stored as a signed 32-bit integer. Therefore, if the system is running continuously, the value of the TickCount property for about 24.9 days will increase, starting with zero and ending with the value Int32.MaxValue, after which it will be reset to the value Int32.MinValue, which is a negative number, and will begin to grow again zero in the next 24.9 days.

Using this property corresponds to a call to the GetTickCount () system function, which is very fast, since it simply returns the value of the corresponding counter. However, its accuracy is low (10 milliseconds), since the interrupts generated by the real-time clock of the computer are used to increase the counter.

Conclusion


The Windows operating system contains many timers (functions that allow to measure time intervals). Some of them are accurate, but not fast (timeGetTime), others are fast, but not accurate (GetTickCount, GetSystemTime), and still others, according to Microsoft, are fast and accurate. The latter include the HPET timer and functions that allow it to work with it: QueryPerformanceFrequency, QueryPerformanceCounter.

The Stopwatch class is actually a managed wrapper over HPET. The use of this class has both advantages (more accurate measurement of time intervals) and disadvantages (errors in the BIOS, HAL can lead to incorrect results), and in the absence of HPET, its advantages are completely lost.

To use or not to use the Stopwatch class is up to you. However, it seems to me that the advantages of this class are still more than disadvantages.

Source: https://habr.com/ru/post/226279/


All Articles