И всё-таки, возможен ли 1мс таймер в Windows?

Вся суть
Вся суть

В комментариях к недавней статье оказалось что, во-первых, этот вопрос кому-то да и интересен, и, во-вторых, существует некоторое количество заблуждений на эту тему.

Вводные: нам нужен таймер, на Windows, с точностью порядка 1мс, драйвер при этом мы писать не хотим и решения при исполнении которых процессор попытается радикально ускорить глобальное потепление не приемлем.

Есть ли такое решение? Из коробки — нету, но при помощи нехитрых приспособлений наше досадное недоразумение превращается… в точный таймер, конечно же.

У нас есть некоторое количество досадных недоразумений системных API которые с каждой новой весией Windows всё сильнее ужимают с целью экономии батареи на ноутбуках, общий обзор можно посмотреть в статье по ссылке в самом начале, с графиками. В целом, можно сказать что сколько-нибудь удовлетворительный тайминг начинается примерно со 100мс, всё что ниже чем 15.6мс за гранью допустимого (по мнению ребят из Microsoft). Да и вообще, 640КБ ну точно хватит всем, правда?

Ну а временные промежутки меньше 1мс вообще немыслимы — большинство API даже не принимает таких значений, не говоря уже о корректной работе с ними.

Исходя из этого я буду строить своё решение вокруг трех недокументированных функций API Win32: NtQueryTimerResolution, NtSetTimerResolution, NtDelayExecution.
Связка из первых двух позволяет добиться разрешения системного таймера меньше 1мс, а третья — воспользоваться этим дополнительным разрешением для сна с точностью менее 1мс.

Итак, начнем: я пишу преимущественно на C#, но на любом многих ЯП можно написать всё точно то же самое.

Шаг 0: поднимем разрешение до максимального. Начиная с Win10 2004 это разрешение больше не является глобальным так что можно ни в чём себе не отказывать (с другой стороны — если процесс не поднял себе разрешение то оно будет 15.6мс вне зависимости от того что там в «глобальном» параметре).

[DllImport("ntdll.dll", SetLastError = true)]
static extern int NtQueryTimerResolution(out int MinimumResolution, out int MaximumResolution, out int CurrentResolution);
[DllImport("ntdll.dll", SetLastError = true)]
static extern int NtSetTimerResolution(int DesiredResolution, bool SetResolution, out int CurrentResolution); private static void AdjustTimerResolution()
{ var queryResult = NtQueryTimerResolution(out var min, out var max, out var current); if (queryResult != 0) return; _systemTimerResolution = TimeSpan.FromTicks(current); if (NtSetTimerResolution(max, true, out _) == 0) { _systemTimerResolution = TimeSpan.FromTicks(max); }
}

Шаг 1: создадим класс PreciseTimer. Полный код я привести, увы, не могу но общая структура такова: поток с максимальным приоритетом который крутится в while(true) цикле и следущие важные поля:

// Период срабатывания
private TimeSpan _period;
// Время прошедшее от последнего срабатывания
private readonly Stopwatch _sw = Stopwatch.StartNew();
// Время оставшееся до следующего срабатывания
public TimeSpan Remaining => _period - _sw.Elapsed;
// Таймер уничтожен и должен быть остановлен
private bool _disposed;

Приметка для людей которые не пишут на C#: Stopwatch это обертка над Win32 методамиQueryPerformanceFrequency и QueryPerformanceCounter, никакой дополнительной магии нету.

Шаг 2: выясним сколько же нам спать. И спим!

private static void TimerTick()
{ // Реализацию выбора следующего таймера оставим пытливым читателям PreciseTimer nextTimer = GetNextTimer(); while (!nextTimer._disposed) { var remaining = nextTimer.Remaining; if (remaining > _systemTimerResolution) { // Если разрешение системного таймера позволяет - спим SleepPrecise(remaining); continue; } // Когда разрешение уже не позволяет спать - спиним while (nextTimer.Remaining > TimeSpan.Zero) { // YieldProcessor(), для X86 это инструкция REP NOP Thread.SpinWait(1000); } // Дождались: тикаем! nextTimer.Tick(); break; }
} // Функция unsafe потому что автор кода - ленивая жопа
// Перед броском гнилым помидором подумайте: хотелось бы вам выделять память вручную?
[DllImport("ntdll.dll", SetLastError = true)]
static unsafe extern int NtDelayExecution(bool alertable, long* delayInterval); private static unsafe void SleepPrecise(TimeSpan timeToSleep)
{ // Посчитаем число целых периодов сна, округлим отбрасываем дробной части var periods = (int)(timeToSleep.TotalMilliseconds / _systemTimerResolution.TotalMilliseconds); if (periods == 0) return; // И спим! var ticks = -(_systemTimerResolution.Ticks * periods); NtDelayExecution(false, &ticks);
}

Шаг 3: посмотрим что из этого вышло: запустим таймер на 1 минуту и запишем полученные промежутки времени. Код обвязки был использован тоже из статьи по линку в начале, но, к сожалению, там нет кода чтобы построить те великолепные графики, поэтому… не стреляйте в программиста, он рисует как умеет.

Тесты запускались на Ryzen 9 5950X под управлением Win11 версии 22000.469
Среднее значение для таймера в 1мс: 1.022мс, stddev = 0.018

Распределение тиков, таймер 1мс
Распределение тиков, таймер 1мс

Для таймера в 10мс: 10.022мс, stddev = 0.017

Распределение тиков, таймер 10мс
Распределение тиков, таймер 10мс

Очевидно, что алгоритм можно слегка улучшить учитывая время лага и дожать среднее до целевого, но это тоже останется задачей для пытливых читателей.

С загрузкой процессора вопрос интереснее, в целом можно утверждать что обнаружению она не поддается: все утилиты радостно рапортируют о 0% загрузке. Установив вручную Affinity на конкретное ядро процессора ничего интересного тоже не обнаружено:

А вы угадаете на каком ядре сейчас вовсю жарит 1мс таймер?
А вы угадаете на каком ядре сейчас вовсю жарит 1мс таймер?

Подводя итог: цель достигнута? Мне кажется что ответ «да».

Все сниппеты кода вдохновлены реальными событиями и использованы в продакшне.

Читайте так же:

  • Как не потерять целевой трафик на сайт: советы вебмастеруКак не потерять целевой трафик на сайт: советы вебмастеру Доход вебмастера напрямую зависит от трафика на сайт. Чем больше трафик, тем больше прибыль. Поэтому если посещаемость резко снижается, нужно оперативно выявить причину и устранить ее. Трафик может просесть даже у хорошо оптимизированного сайта, однако чаще это связано с ошибками, […]
  • TikTok Marketing Meetup от SLON Media Описание 24 июня в 18.00 по Минску (UTC+3) команда SLON Media расскажет о том. Как использовать TikTok для продвижения бизнеса. Будь то разработка мобильных приложений или e-commerce. Регистрация по ссылке обязательна!Доклады:1) Медиабаинг в TikTok или как получить результаты за […]
  • Xiaomi Mi Mix 4 будет оснащён лучшей подэкранной камерой на рынке. Сравнительные фото экранов от Ice UniverseXiaomi Mi Mix 4 будет оснащён лучшей подэкранной камерой на рынке. Сравнительные фото экранов от Ice Universe Проверенный информатор под ником Ice Universe утверждает. Что в смартфоне Xiaomi Mi Mix 4 реализована лучшая подэкранная селфи-камера на рынке. Он также опубликовал макроснимки. Демонстрирующие расположение пикселей в области. Где находится подэкранная камера. И в остальной части […]
  • Договор наполнения сайтаДоговор наполнения сайта Что такое контрактная упаковка? Вместо того, чтобы упаковать свою продукцию внутри компании. Производитель или розничный торговец может передать работу на аутсорсинг контрактной упаковочной компании (часто называемой контрактным упаковщиком). Упаковщик контракта затем упаковывает […]