Введение
Очень часто, нам разработчикам необходимо измерить время выполнения своего (и не только своего) кода. Когда я только начал программировать, я использовал структуру DateTime для этих целей. Прошло время, и я узнал о классе Stopwatch и начал его активно использовать. Думаю аналогичная ситуация была и у вас. Не то, чтобы я раньше не задавался вопросом о том, как работает Stopwatch, просто на тот момент знаний о том, что он позволяет измерять затраченное время точнее, чем DateTime мне хватало. Пришло время разъяснить себе, а так же читателям то, как на самом деле работает класс Stopwatch, а так же выяснить его преимущества и недостатки по сравнению с использованием DateTime.
Использование DateTime
Использовать структуру DateTime для замера времени выполнения кода достаточно просто:
Свойство DateTime.Now — возвращает локальную текущую дату и время. Вместо свойства DateTime.Now можно использовать свойство DateTime.UtcNow — возвращающее текущую дату и время, но вместо локального часового пояса оно представляет их как время Utc, то есть как всемирное координированное время.
Несколько слов о структуре DateTime
Возможно, немногие задумывались о том, что из себя представляет структура DateTime. Значение структуры DateTime измеряется в 100-наносекундных единицах, называемых тактами, и точная дата представляется числом тактов прошедших с 00:00 1 января 0001 года нашей эры.
Например, число 628539264000000000 представляет собой 6 октября 1992 года 00:00:00.
Структура DateTime содержит единственное поле, которое и содержит количество прошедших тактов:
Следует так же сказать, что начиная с .NET 2.0, 2 старших бита данного поля указывают тип DateTime: Unspecfied — не задан, Utc — координированное время, Local — местное время, а остальные 62 бита — количество тактов. Мы можем легко запросить эти два бита с помощью свойства Kind.
Что плохого в использовании DateTime?
Вычисление свойства DateTime.Now основывается на DateTime.UtcNow, то есть сначала вычисляется координированное время, а потом к нему применяется смещение часового пояса.
Именно поэтому использовать свойство DateTime.UtcNow будет правильнее, оно вычисляется намного быстрее:
Проблема использования DateTime.Now или DateTime.UtcNow заключается в том, что их точность фиксирована. Как было сказано выше
1 tick = 100 nanoseconds = 0.1 microseconds = 0.0001 milliseconds = 0.0000001 seconds
соответственно измерить временной интервал длина которого меньше чем длинна одного такта, просто невозможно. Конечно, маловероятно, что вам это потребуется, но знать это надо.
Использование класса Stopwatch
Класс Stopwatch появился в .NET 2.0 и с тех по не претерпел ни одного изменения. Он предоставляет набор методов и средств, которые можно использовать для точного измерения затраченного времени.
Публичный API класса Stopwatch выглядит следующий образом:
Свойства
- Elapsed — возвращает общее затраченное время;
- ElapsedMilliseconds — возвращает общее затраченное время в миллисекундах;
- ElapsedTicks — возвращает общее затраченное время в тактах таймера;
- IsRunning — возвращает значение, показывающее, запущен ли таймер Stopwatch.
Методы
- Reset — останавливает измерение интервала времени и обнуляет затраченное время;
- Restart — останавливает измерение интервала времени, обнуляет затраченное время и начинает измерение затраченного времени;
- Start — запускает или продолжает измерение затраченного времени для интервала;
- StartNew — инициализирует новый экземпляр Stopwatch, задает свойство затраченного времени равным нулю и запускает измерение затраченного времени;
- Stop — останавливает измерение затраченного времени для интервала.
Поля
- Frequency — возвращает частоту таймера, как число тактов в секунду;
- IsHighResolution — указывает, зависит ли таймер от счетчика производительности высокого разрешения.
Код, использующий класс Stopwatch для измерения времени выполнения метода SomeOperation может выглядеть так:
Первые две строчки можно записать более лаконично:
Реализация Stopwatch
Класс Stopwatch основан на HPET (High Precision Event Timer, таймер событий высокой точности). Данный таймер был введён фирмой Microsoft, чтобы раз и навсегда поставить точку в проблемах измерения времени. Частота этого таймера (минимум 10 МГц) не меняется во время работы системы. Для каждой системы Windows сама определяет, с помощью каких устройств реализовать этот таймер.
Класс Stopwatch содержит следующие поля:
TicksPerMillisecond — определяет количество DateTime тактов в 1 миллисекунду;
TicksPerSecond — определяет количество DateTime тактов в 1 секунду;
isRunning — определяет, запущен ли текущий экземпляр (вызван ли был метод Start);
startTimeStamp — число тактов на момент запуска;
elapsed — общее число затраченных тактов;
tickFrequency — упрощает перевод тактов Stopwatch в такты DateTime.
Статический конструктор проверяет наличие таймера HPET и в случае его отсутствия частота Stopwatch устанавливается равной частоте DateTime.
Основной сценарий работы данного класса был показан выше: вызов метода Start, метод время которого необходимо измерить, а затем вызов метода Stop.
Реализация метода Start очень проста — он запоминает начальное число тактов:
Следует сказать, что вызов метода Start на уже замеряющем экземпляре ни к чему не приводит.
Аналогично просто устроен метод Stop:
Вызов метода Stop на остановленном экземпляре так же ни к чему не приводит.
Оба метода используют вызов GetTimestamp() — возвращающего количество тактов на момент вызова:
При наличии HPET(таймер событий высокой точности) такты Stopwatch отличаются от тактов DateTime.
Следующий код
на моем компьютере выводит
Использовать такты Stopwatch для создания DateTime или TimeSpan неверно. Запись
по понятным причинам приведет к неправильным результатам.
Чтобы получить такты DateTime, а не Stopwatch нужно воспользоваться свойствами Elapsed и ElapsedMilliseconds или же сделать преобразование вручную. Для преобразования тактов Stopwatch в такты DateTime в классе используется следующий метод:
Код свойств выглядит, как и ожидалось:
Что плохого в использовании Stopwatch?
Примечание к данному классу с MSDN говорит: на многопроцессорном компьютере не имеет значения, на каком из процессоров выполняется поток. Однако, из-за ошибок в BIOS или слое абстрагированного оборудования (HAL), можно получить различные результаты расчета времени на различных процессорах.
Во избежание этого в методе Stop стоит условие if (elapsed < 0).
Я нашел немало статей, авторы которых столкнулись с проблемами из-за некорректной работы HPET.
В случае отсутствия HPET Stopwatch использует такты DateTime, поэтому его преимущество перед явным использованием DateTime теряется. К тому же нужно учитывать время на вызовы методов и проверки осуществляемые Stopwatch, особенно если это происходит в цикле.
Stopwatch in mono
Мне стало интересно, как реализован класс Stopwatch в mono, поскольку рассчитывать на нативные функции Windows по работе с HPET не приходится.
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 решать Вам. Однако как мне кажется преимуществ у данного класса, все же больше чем недостатков.
Комментариев нет:
Отправить комментарий