Під капотом у Stopwatch

    

Введення

Дуже часто, нам розробникам необхідно виміряти час виконання свого (і не тільки свого) коду. Коли я тільки почав програмувати, я використовував структуру DateTime для цих цілей. Минув час, і я дізнався про клас Stopwatch і почав його активно використовувати. Думаю аналогічна ситуація була і у вас. Не те, щоб я раніше не задавався питанням про те, як працює Stopwatch, просто на той момент знань про те, що він дозволяє вимірювати витрачений час точніше, ніж DateTime мені вистачало. Прийшов час роз'яснити собі, а так само читачам те, як насправді працює клас Stopwatch, а так само з'ясувати його переваги і недоліки в порівнянні з використанням DateTime.
 
 

Використання DateTime

Використовувати структуру DateTime для виміру часу виконання коду досить просто:
 
 
var before = DateTime.Now;
SomeOperation();
var spendTime = DateTime.Now - before;

Властивість DateTime.Now — повертає локальну поточну дату і час. Замість властивості DateTime.Now можна використовувати властивість DateTime.UtcNow — повертає поточну дату і час, але замість локального годинного пояса воно представляє їх як час Utc, тобто як всесвітній координований час.
 
 
var before = DateTime.UtcNow;
SomeOperation();
var spendTime = DateTime.UtcNow - before;        

 

Кілька слів про структуру DateTime

Можливо, мало хто замислювався про те, що з себе представляє структура DateTime. Значення структури DateTime вимірюється в 100-наносекундних одиницях, званих тактами, і точна дата представляється числом тактів минули з 00:00 1 січня 0001 нашої ери.
 
Наприклад, число 628539264000000000 являє собою 6 жовтня 1992 00:00:00.
 
Структура DateTime містить єдине поле, яке і містить кількість пройшли тактів:
 
 
private UInt64 dateData;

Слід так само сказати, що починаючи з. NET 2.0, 2 старших біта даного поля вказують тип DateTime: Unspecfied — не заданий, Utc — координований час, Local — місцевий час, а решта 62 біта — кількість тактів. Ми можемо легко запросити ці два біти за допомогою властивості Kind.
 
 

Що поганого у використанні DateTime?

Використовувати властивість DateTime.Now для вимірювання часових інтервалів не дуже гарна ідея, і ось чому:
 
 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);
       }
}

 
Обчислення властивості DateTime.Now грунтується на DateTime.UtcNow, тобто спочатку обчислюється координований час, а потім до нього застосовується зсув годинного пояса.
 
Саме тому використовувати властивість DateTime.UtcNow буде правильніше, воно обчислюється набагато швидше:
 
 DateTime.UtcNow
public static DateTime UtcNow
{
  get
    {
       long ticks = 0;
       ticks = GetSystemTimeAsFileTime();
       return new DateTime(((UInt64)(ticks + FileTimeOffset)) | KindUtc);
    }
}

 
Проблема використання DateTime.Now або DateTime.UtcNow полягає в тому, що їх точність фіксована. Як було сказано вище
 
 1 tick = 100 nanoseconds = 0.1 microseconds = 0.0001 milliseconds = 0.0000001 seconds
 
відповідно виміряти часовий інтервал довжина якого менше ніж довжина одного такту, просто неможливо. Звичайно, малоймовірно, що вам це буде потрібно, але знати це треба.
 
 

Використання класу Stopwatch

Клас Stopwatch з'явився в. NET 2.0 і з тих по не зазнав жодної зміни. Він надає набір методів і засобів, які можна використовувати для точного виміру витраченого часу.
 
Публічний API класу Stopwatch виглядає наступний чином:
 
 Властивості
 
     
  1. Elapsed — повертає загальне витрачений час;
  2.  
  3. ElapsedMilliseconds — повертає загальне витрачений час в мілісекундах;
  4.  
  5. ElapsedTicks — повертає загальне витрачений час в тактах таймера;
  6.  
  7. IsRunning — повертає значення, що показує, чи запущений таймер Stopwatch.
  8.  
 Методи
 
     
  1. Reset — зупиняє вимір інтервалу часу і обнуляє витрачений час;
  2.  
  3. Restart — зупиняє вимір інтервалу часу, обнуляє витрачений час і починає вимір витраченого часу;
  4.  
  5. Start — запускає або продовжує вимір витраченого часу для інтервалу;
  6.  
  7. StartNew — ініціалізує новий екземпляр Stopwatch, задає властивість витраченого часу рівним нулю і запускає вимір витраченого часу;
  8.  
  9. Stop — зупиняє вимір витраченого часу для інтервалу.
  10.  
 Поля
 
     
  1. Frequency — повертає частоту таймера, як число тактів в секунду;
  2.  
  3. IsHighResolution — вказує, чи залежить таймер від лічильника продуктивності високого дозволу.
  4.  
Код, що використовує клас Stopwatch для вимірювання часу виконання методу SomeOperation може виглядати так:
 
 
var sw = new Stopwatch();
sw.Start();
SomeOperation();
sw.Stop();

Перші два рядки можна записати більш лаконічно:
 
 
var sw = Stopwatch.StartNew();
SomeOperation();
sw.Stop();

 

Реалізація Stopwatch

Клас Stopwatch заснований на HPET (High Precision Event Timer, таймер подій високої точності). Даний таймер був введений фірмою Microsoft, щоб раз і назавжди поставити крапку в проблемах виміру часу. Частота цього таймера (мінімум 10 МГц) не змінюється під час роботи системи. Для кожної системи Windows сама визначає, з допомогою яких пристроїв реалізувати цей таймер.
 
Клас Stopwatch містить наступні поля:
 
 
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 — визначає кількість DateTime тактів в 1 мілісекунду;
TicksPerSecond — визначає кількість DateTime тактів в 1 секунду;
 
isRunning — визначає, чи запущений поточний примірник (викликаний чи був метод Start);
startTimeStamp — число тактів на момент запуску;
elapsed — загальне число витрачених тактів;
 
tickFrequency — спрощує переклад тактів Stopwatch в такти DateTime.
 
Статичний конструктор перевіряє наявність таймера HPET і в разі його відсутності частота Stopwatch встановлюється рівною частоті DateTime.
 
 Статичний конструктор Stopwatch
static Stopwatch() 
{                       
  bool succeeded = SafeNativeMethods.QueryPerformanceFrequency(out Frequency);            
    if(!succeeded) 
     {
        IsHighResolution = false; 
        Frequency = TicksPerSecond;
        tickFrequency = 1;
      }
     else 
     {
        IsHighResolution = true;
        tickFrequency = TicksPerSecond;
        tickFrequency /= Frequency;
     }   
}

 
Основний сценарій роботи даного класу був показаний вище: виклик методу Start, метод час якого необхідно виміряти, а потім виклик методу Stop.
 
Реалізація методу Start дуже проста — він запам'ятовує початкове число тактів:
 
 Start
public void Start()
{
   if (!isRunning)
     {
       startTimeStamp = GetTimestamp();
       isRunning = true;
     }
}

 
 Слід сказати, що виклик методу Start на вже заміряли екземплярі ні до чого не приводить.
 
Аналогічно просто влаштований метод Stop:
 
 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
          // (e.g. Intel SpeedStep).

          elapsed = 0;
         }
      }
}

 
 Виклик методу Stop на зупиненому екземплярі так само ні до чого не приводить.
 
Обидва методи використовують виклик GetTimestamp () — повертає кількість тактів на момент виклику:
 
 GetTimestamp
public static long GetTimestamp()
{
   if (IsHighResolution)
     {
       long timestamp = 0;
       SafeNativeMethods.QueryPerformanceCounter(out timestamp);
       return timestamp;
     }
     else
      {
         return DateTime.UtcNow.Ticks;
      }
 }

 
При наявності HPET (таймер подій високої точності) такти Stopwatch відрізняються від тактів DateTime.
 
Наступний код
 
 
Console.WriteLine(Stopwatch.GetTimestamp());
Console.WriteLine(DateTime.UtcNow.Ticks);

на моєму комп'ютері виводить
 
 
5201678165559
635382513439102209

Використовувати такти Stopwatch для створення DateTime або TimeSpan невірно. Запис
 
 
var time = new TimeSpan(sw.ElaspedTicks);

зі зрозумілих причин призведе до неправильних результатів.
 
Щоб отримати такти DateTime, а не Stopwatch потрібно скористатися властивостями Elapsed і ElapsedMilliseconds або ж зробити перетворення вручну. Для перетворення тактів Stopwatch в такти DateTime в класі використовується наступний метод:
 
 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;
      }
}

 
Код властивостей виглядає, як і очікувалося:
  
 Elapsed, ElapsedMilliseconds
public TimeSpan Elapsed
{
   get { return new TimeSpan(GetElapsedDateTimeTicks()); }
}

public long ElapsedMilliseconds
{
  get { return GetElapsedDateTimeTicks() / TicksPerMillisecond; }
}

 
 

Що поганого у використанні Stopwatch?

Примітка до даного класу з MSDN каже: на многопроцессорном комп'ютері не має значення, на якому з процесорів виконується потік. Однак, через помилки в BIOS або шарі абстрагованого обладнання (HAL), можна отримати різні результати розрахунку часу на різних процесорах.
 
Щоб уникнути цього в методі Stop стоїть умова if (elapsed <0).
 
Я знайшов чимало статей , автори яких зіткнулися з проблемами через некоректної роботи HPET.
 
У разі відсутності HPET Stopwatch використовує такти DateTime, тому його перевага перед явним використанням DateTime втрачається. До того ж потрібно враховувати час на виклики методів та перевірки здійснюються Stopwatch, особливо якщо це відбувається в циклі.
 
 

Stopwatch in mono

Мені стало цікаво, як реалізований клас Stopwatch в mono, оскільки розраховувати на нативні функції Windows по роботі з HPET не доводиться.
 
 
public static readonly long Frequency = 10000000;
public static readonly bool IsHighResolution = true;

Stopwatch в mono використовує завжди такти DateTime, а тому переваг перед явним використанням DateTime у нього немає, хіба, що код більш читабельний.
 
 

Environment.TickCount

Слід так само сказати про властивість Environment.TickCount, яке повертає час, минуле з моменту завантаження системи (у мілісекундах).
 
Значення цієї властивості витягується з таймера системи і зберігається як ціле 32-розрядне число зі знаком. Отже, якщо система працює безперервно, значення властивості TickCount протягом приблизно 24,9 днів буде зростати, починаючи з нуля і закінчуючи значенням Int32.MaxValue, після чого воно буде скинуто до значення Int32.MinValue, що є негативним числом, і знову почне рости до нуля протягом наступних 24,9 днів.
 
Використання даної властивості відповідає викликом системної функції GetTickCount (), яка є дуже швидкою, так як просто повертає значення відповідного лічильника. Однак точність її низька (10 мілісекунд), оскільки для збільшення лічильника використовуються переривання, що генеруються годинником реального часу комп'ютера.
 
 

Висновок

Операційна система Windows містить чимало таймерів (функцій дозволяють вимірювати інтервали часу). Одні з них точні, але не швидкі (timeGetTime), інші швидкі, але не точні (GetTickCount, GetSystemTime), а третє як стверджує Microsoft і швидкі і точні. До числа останніх відноситься таймер HPET і функції, що дозволяють з ним працювати: QueryPerformanceFrequency, QueryPerformanceCounter.
 
Клас Stopwatch фактично є керованою обгорткою над HPET. У використання даного класу є як переваги (більш точне вимірювання часових інтервалів), так і недоліки (помилки в BIOS, HAL можуть призводити до неправильних результатів), а в разі відсутності HPET його переваги і зовсім губляться.
 
Використовувати або не використовувати клас Stopwatch вирішувати Вам. Однак як мені здається переваг у даного класу, все ж більше ніж недоліків.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.