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

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

Вводные: нам нужен таймер, на 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мс таймер?

Должен признать что это best case когда циклы сна полностью совпали с таймером. Поскольку код оптимизирован под точность он иногда будет нагружать одно ядро примерно на 30-40%.
Тут стоит сразу же уточнить что эта нагрузка для процессоров у которых есть HyperThreading (или аналог) относительно безвредная: гарантируется что в это время второй поток сможет исполняться на том же ядре с минимальным ущербом для производительности. У процессоров без оного ситуация хуже, но тесты на ноутбуках показывают что в плане потребления энергии/тепла ситуация гораздо лучше чем у более грубых решений (while(true) цикл без rep nop, например).

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

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

Комментарии (52)


  1. segment
    13.02.2022 16:14

    А если появился новый таймер, как правильно в таком случае сделать останов текущего таймера и перерасчет? Или какое применение у такого таймера?


    1. Alexx999 Автор
      14.02.2022 00:15

      Я постарался урезать код до минимума, как показывает практика читать длинные простыни не слишком интересно.
      Для этого есть alertable wait states, при добавлении нового таймера можно будить поток. Плюс, можно ограничить максимальную длительность сна - даже если всегда спать 0.5мс это не сильно убьет нагрузку на процессор. Ну или ваш вариант :)


      1. nckma
        14.02.2022 09:43
        +2

        А будет ли работать такой "таймер":

        1) открываем файл COM порт на скорости передачи 19200bps в режиме FILE_FLAG_OVERLAPPED

        2) записываем в файл девайса порта 2 байта

        3) ждем completition Event из структуры OVERLAPPED переданной в WriteFile(..)

        При скорости 19200 один бит передается за 0,000052083 секунды, но там байт и еще старт бит и стоп бит, итого 2 байта передается 0,00052083*2=0,00104166 секунды.

        Аппаратное прерывание последовательного порта через драйвер пробуждает поток, который ждет эвента. Все.


        1. Alexx999 Автор
          14.02.2022 14:45

          Далеко не у всех нынче есть COM порт - у меня нет :)
          Но, кстати говоря, есть у меня один pet проект (даталоггер для блока управления двигателем) где используется COM через USB - когда в следующий раз буду ковыряться в нём с осцилографом то попробую.


  1. lieff
    13.02.2022 16:25
    +1

    Не уверен, но возможно документировано это делает timeBeginPeriod(1).


    1. Alexx999 Автор
      14.02.2022 00:17

      Да, но разрешение системного таймера в 1мс сна для устойчивой работы таймера в 1мс - это много. Для таймеров в 3мс и более - вполне множно обойтись документированными функциями.


  1. amarao
    13.02.2022 17:42
    +6

    Смотреть на утилизацию процессора с при увеличении скорости системного таймера глупо. Возьмите ноут, выкиньте всё лишнее из процессов и сравните разряд батареи через N часов аптайма. Я ставлю на то, что на 20-30% больше энергии будет жрать, чем при дефолтных настройках таймера.


    1. Alexx999 Автор
      14.02.2022 00:19
      +1

      У меня дефолтная настройка таймера именно 0.5мс, спасибо WPF, спасибо хрому и (в моем случае) спасибо софту который мониторит напряжения на процессоре.

      Номинально начиная с Win10 2004 это должно влиять гораздо меньше но конкретно в нашем продукте работа от батарей не рассматривается как класс, поэтому тему я не исследовал.


      1. amarao
        14.02.2022 12:25

        2000 тиков в секунду? Жуть какая.

        cat /boot/config-5.15.0-18-generic |grep HZ
        CONFIG_NO_HZ_COMMON=y
        CONFIG_NO_HZ_IDLE=y
        CONFIG_NO_HZ=y
        CONFIG_HZ_250=y
        CONFIG_HZ=250
        


        1. ZimM
          14.02.2022 13:14
          +1

          Начиная с Windows 8, ядро стало tick-less, поэтому прямой связи тут нет.


          1. amarao
            14.02.2022 15:02

            А, они тоже на NO_HZ перешли? Молодцы.


  1. vanxant
    13.02.2022 18:28
    +6

    Комменты в коде самокритичны!


  1. alexeishch
    13.02.2022 18:37
    +6

    Функция unsafe потому что автор кода - ленивая жопа 
    Перед броском гнилым помидором подумайте: хотелось бы вам выделять память вручную?

    Надо было банально прочитать инструкцию. Для таких случаев маршалинга используется ref long


    1. shai_hulud
      13.02.2022 21:29
      +1

      Для тех кто подумал "neat" пора переписывать всё с поинтеров на "ref X" хочу упомянуть, что это приводит к маршалингу т.е. будет сгенерена "ненужная" прокладка через которую будет вызван нативный метод. Не все сценарии требуют лучшего пефоманса, там 'ref' подходят.


    1. Alexx999 Автор
      14.02.2022 00:22

      Да, виноват, в рамках примера это наверное было бы лучше чем С-стайл.


  1. Vindicar
    13.02.2022 19:22
    +2

    Хм. Прикольно, но я затрудняюсь придумать применение.
    Мне несколько лет назад требовалось таймить запросы к USB устройству (адаптер для энкодера), так там время выполнения запроса гуляло так, что в итоге пришлось плюнуть и вынести отслеживание энкодера и сбор данных на ардуинку. Боюсь, с другим вводом-выводом будет примерно то же самое.

    Разве что какой-то специфический race condition ловить в чужом коде?


    1. Revertis
      13.02.2022 19:29

      Ещё несколько лет назад писали тут на Хабре, что Chrome меняет разрешение системного таймера на 1мс, и после закрытия не меняет назад. Видимо для ускорения таймаутов в JS, анимаций и т.п.


    1. Alexx999 Автор
      14.02.2022 00:34
      +2

      Конкретно 1мс таймер? Это код который не имеет применения... Или имеет :)
      У нас в коде используются промежутки 10 мс и более, проблема в том что с приемлемой точностью 10мс отмерить тоже нечем, мультимедиа таймеры точностью не отличились. Т.е. это в первую очередь точный таймер а не быстрый таймер. Просто так случилось что его точности хватает и для промежутков в 1мс и менее, а это уже отличный повод похвалиться успехами, не так ли?


  1. third112
    13.02.2022 20:22
    -2

    А asm команда rdtsc не подойдет? В ЯП Delphi-7 использовал таким образом:


    type
      TCPUTime = record
                  case Boolean of
                  true: (w1,w2 : dword);
                  false: (t: int64);
                  end;
    var
      CPUTime : TCPUTime;
    
    procedure getCPUTime;
    asm
          rdtsc
          mov CPUTime.w1,eax
          mov CPUTime.w2,edx
    end;
    


    1. Alexx999 Автор
      14.02.2022 00:25
      +2

      Команда не спит, команда просто читает время. В случае с Windows такое доступно при помощи QueryPerformanceCounter


      1. third112
        14.02.2022 01:15
        -2

        Извините, не понял, чем мое решение хуже Вашего? (В случае с Windows asm доступен).


        1. Alexx999 Автор
          14.02.2022 01:58
          +3

          Тем что это решение не решает проблему вызова функции раз в 1 мс а просто получает текущее значение счетчика циклов процессора, поведение которого зависит от конкретной модели процессора.

          А вот задача "измерить точное время" это совершенно иная история и по состоянию на сегодня она уже успешно решается стандартными библиотеками - в C# это Stopwatch.StartNew(), в случае с C++ это std::chrono::steady_clock::now(), а для C (и других языков) даже без ASM в Win32 доступен QueryPerformanceCounter


          1. progman78
            14.02.2022 13:15
            -1

            Тем что это решение не решает проблему вызова функции раз в 1 мс а просто получает текущее значение счетчика циклов процессора, поведение которого зависит от конкретной модели процессора.

            создали поток, в нем через RDTSC ( она кстати не на всех CPU у меня работала ) постоянно читаем время. Если прошло х мс - дергаем каллбэк функцию.

            Минус - придется как то шаманить чтобы поток не занимал ядро на 100%. Сходу ничего придумать не могу пока. NOP`ами если только цикл забивать... но не думаю что сработат.


            1. Alexx999 Автор
              14.02.2022 14:20
              +4

              Это в статье по ссылке в начале есть - вариант "while цикл", и, собственно, вся сложность и состоит в том чтобы поток не занимал ядро на 100%


              1. progman78
                15.02.2022 06:43

                RDTSC - 10 тактов. если мы в цикле постоянно измеряем время и хотим загрузку проца <1% логично что нам нужна задержка cpu pause на 990 тактов

                REP NOP

                NOP 3 такта. REP 1 такт. итого 4 такта. 250 инструкций вставляем после RDTSC. По идее дожно сработать.

                Или использовать PAUSE  инструкцию что по сути одно и то же.


              1. progman78
                15.02.2022 12:11

                У меня получилось "запаузить" cpu в варианте "while цикл" :

                uint64_t clockCycleCountPerSecond	= 0;
                uint64_t startTimerTime						= 0;
                
                uint64_t rdtsc()
                {
                	uint64_t c;
                
                	__asm
                	{
                		rdtsc
                		mov dword ptr[c + 0], eax
                		mov dword ptr[c + 4], edx
                	}
                
                	return c;
                }
                
                void init()
                {
                	uint64_t t1 = rdtsc();
                	Sleep(1000);
                	uint64_t t2 = rdtsc();
                
                	clockCycleCountPerSecond = (t2 - t1) / 1000;
                }
                
                int main()
                {
                	init();
                
                	new boost::thread( boost::bind( &timerThread ) );
                
                	printf("Enter character: ");
                	getchar();
                }
                
                void	timerThread()
                {
                	startTimerTime = rdtsc();
                
                	for (;;)
                	{
                		if ( rdtsc_2_ms() >= 1 )
                		{
                			oneMsTimerFunc();
                
                			startTimerTime = rdtsc();
                		}
                
                		cpu_sleep();
                	}
                
                	return;
                }
                
                void	oneMsTimerFunc()
                {
                	std::cout << "oneMsTimerFunc\tms=" << rdtsc_2_ms()<< "\n";
                }
                

                функцию cpu_sleep() не привожу так как она занимает 16 килобайт кода забитые __asm NOP. При меньшем количестве nop загрузка CPU значительная. Ниже 12% загрузки CPU опустить не получилось.

                как оно себя на одноядерной машине вести будет не знаю - у меня такого раритета нет. Скорее всего будет жутко лагать или винда будет все равно переключать потоки и 1мс тайминг добиться не получится совсем.


                1. Alexx999 Автор
                  15.02.2022 15:17
                  +1

                  Собственно проблема в том что пока ядро исполняет нопы - оно не исполняет ничего полезного (а могло бы), поэтому уступить квант времени - априори оптимальнее.

                  Далее, если уж писать на плюсах то всю работу с rdtsc можно заменить на вызовы std::chrono::steady_clock::now() (номинально std::chrono::high_resolution_clock::now(), но учитывая "нюансы с реализациями" steady_clock будет более переносимым решением).

                  Ну и, наверное, стоит уточнить что вся суть моей статьи - это как сделать таймер на 1мс не скатываясь до цикла с нопами. Цикл с нопами работает, и он у меня в коде тоже есть, но цель - крутить нопы только в случае крайней необходимости.


                  1. progman78
                    15.02.2022 15:37

                    Собственно проблема в том что пока ядро исполняет нопы - оно не исполняет ничего полезного

                    ну когда у нас ядра растут как на дрожжах это уже не является проблемой. У меня половина ядер ничего полезного не исполняет вот совсем. Это проблема на машине с 2мя, 4мя ядрами. Когда ядер 16 24 или все 32 то это не проблема КМК.

                    Ну и, наверное, стоит уточнить что вся суть моей статьи - это как сделать таймер на 1мс не скатываясь до цикла с нопами.

                    ну использовать недокументироыванные функции это тоже не решение. Завтра выйдет обновление и досвидос.

                    цитирую вас:

                    Исходя из этого я буду строить своё решение вокруг трех недокументированных функций API Win32: NtQueryTimerResolutionNtSetTimerResolutionNtDelayExecution.

                    поэтому правильное решение все таки только через драйвер идти.

                    Далее, если уж писать на плюсах то всю работу с rdtsc можно заменить на вызовы std::chrono::steady_clock::now()

                    так то вы правы. Но что у меня под капотом в rdtsc -я знаю, и знаю что 10 тактов занимает вызов. а что там они нахимичилит в std::chrono::steady_clock::now() - не знаю ( вернее знаю и мне не нравится что там под капотом )


                    1. Alexx999 Автор
                      15.02.2022 17:14

                      ну когда у нас ядра растут как на дрожжах это уже не является проблемой

                      В этом и вся суть - то что для вас не является проблемой, для нас является. Сценарии есть разные, я же не заставляю везде использовать предложенный таймер :)
                      К примеру, наш продукт состоит кучи процессов-сервисов большинство из которых содержит таймер, обычно по несколько процессов на машине, в вырожденных случаях 10 и более. Ну а машины - не наши, есть и домашние пользователи с 4 ядрами и даже двухьядерники попадаются на вспомогательных машинах.
                      И как раз недавно в таймере был баг который приводил к тому что он крутил очень много нопов - в итоге машину только с кнопки можно было перезагрузить, глухое зависание
                      Предложенный таймер прекрасно справлялся с задачей на том же железе пока я туда не влез и не поломал, ни до ни после жалоб не было.

                      ну использовать недокументироыванные функции это тоже не решение. Завтра выйдет обновление и досвидос.

                      Тут стоит понимать что "недокументированные функции" тоже бывают разными. NtDelayExecution существует с доисторических времен (не удивлюсь если аналог был ещё на OS/2), это KeDelayExecutionThread, другие две функции добавлены в Windows NT 3.5 в 94м году и уже тоже документированы на уровне ядра: ExQueryTimerResolution и ExSetTimerResolution. В целом, я готов поспорить что они никуда не собираются в ближайшем будущем.

                      поэтому правильное решение все таки только через драйвер идти.

                      Да, это так. Зря что у меня нету сертификата которым можно этот драйвер подписать :)

                      и знаю что 10 тактов занимает вызов

                      Будем честны - не всё так просто

                      вернее знаю и мне не нравится что там под капотом

                      В случае с Windows и MSVC это будет QueryPerformanceCounter, который, в случае со свежими версиями Windows и процессором где есть Invariant TSC, будет использовать rdtsc (если хочется проверить лично - смотреть внутрь функции RtlQueryPerformanceCounter).
                      В случае с Linux логично предположить что будет использоваться clock_gettime с параметром CLOCK_MONOTONIC который, совершенно внезапно, точно так же использует rdtsc в случае если процессор поддерживает Invariant TSC.


                      1. progman78
                        16.02.2022 07:07

                        К примеру, наш продукт состоит кучи процессов-сервисов большинство из которых содержит таймер, обычно по несколько процессов на машине, в вырожденных случаях 10 и более. Ну а машины - не наши, есть и домашние пользователи с 4 ядрами и даже двухьядерники попадаются на вспомогательных машинах.

                        драйвер делайте и не придется чесать левую пятку через правое плечо :).

                        Тут стоит понимать что "недокументированные функции" тоже бывают разными.

                        да. Но на то они и недокументированные что ничего не гарантируют. Она 30 лет может в системе быть а потом возьмут и в новой сборке ее уберут или параметры изменят.

                        В случае с Windows и MSVC это будет QueryPerformanceCounter

                        так то да и если покопаться в исходниках это все нарыть можно. Но там обертка на обертке и есть преобразования. А с вызовом rdtsc напрмую все понятно - из проца читаются в EAX/EDX такты и все. Я понимаю что QueryPerformanceCounter по феншую вызывать но блин мне rdtsc больше по душе.


          1. third112
            14.02.2022 13:59
            -1

            Как написано в статье Performance measurements with RDTSC:


            You should not convert your results to seconds. Report them in clock cycles.

            From user's point of view, execution time in seconds makes more sense than clock cycles. But remember that:

            time_in_seconds = number_of_clock_cycles / frequency

            Отсюда тривиальное решение:
            Задаем time_in_seconds = 0.001
            и считаем сколько number_of_clock_cycles должно пройти,
            как пройдет, так ok


            А переводить каждый раз во время не надо.


  1. Sap_ru
    13.02.2022 20:36
    +2

    Мультимедиа таймеры, нет?


    1. alex_dow
      13.02.2022 23:46

      Согласен, как минимум миди кривое на винде, по сравнению с железными секвенсорами, стробит куда хочет то вперед, то назад, правда далеко не для всякой музыки нужен хороший миди тайминг


      1. NTDLL
        14.02.2022 01:09

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


        1. Melanchall
          14.02.2022 10:41

          А почему вы считаете, что подсистема MIDI должна отсутствовать в Windows или любой другой ОС? Она присутствует, потому что MIDI – протокол, активно используемый в музыкальном мире. Введённый в начале 80-х, он живее всех живых. Более того, 2 года назад был выкачен MIDI 2.0 (который крайне медленно набирает обороты, но это другая история).

          MIDI API есть в Windows, в macOS, и очень хорошо, что он есть. В Windows есть новый API, но у него свои болячки.


          1. alex_dow
            14.02.2022 11:03

            Да, использование конвертера юсб-миди - это работает, но это не про хороший тайминг миди. Правда я не совсем понимаю, имеет ли отношение (и может ли иметь) тема статьи к миди?


            1. Alexx999 Автор
              14.02.2022 14:51
              +1

              Ну вот первоначальная статья про 1мс тайминг посвящена именно проблемам с MIDI - насколько я понял, в Windows при выводе MIDI нет нормальной буферизации и нужно софтварно реализовать тайминг.


          1. NTDLL
            14.02.2022 15:12

            Насколько я знаю, MIDI в винде был нужен, чтобы воспроизводить системные звуки. А сейчас он ни к чему, есть формат wav. Но, по непонятным мне причинам, MIDI подсистема осталась.


            1. Melanchall
              14.02.2022 18:09

              Вы путаете MIDI и WAV. MIDI никак не связан с продуцированием звука, это протокол общения между, например, компьютером и синтезатором. MIDI активно используется, как средство передачи партий в музыкальной среде. Например, можно записать партию ударных в MIDI, и вывести этот MIDI разными сэмплерами, получая уже как раз WAV. К системным звукам Windows протокол отношения не имеет. Сама подсистема в ОС нужна, и хорошо, что она существует.


    1. Alexx999 Автор
      14.02.2022 00:28
      +1

      Пробовали, у них большой разброс - иногда таймер опаздывает, иногда спешит, иногда вообще не работает. Собственно мне 1мс интервал не нужен (просто оказалось что мой таймер так может, в продакшне у нас не применяется), таймер был написан именно из-за проблем с мультимедиа таймером.


  1. Dmitriy_Volkov
    14.02.2022 02:44
    +2

    Лет 20 назад делал эксперименты и писал vxd драйвера. В частности программировал таймер и обрабатывал прерывания. Так вот, меняя делитель, настроить частоту проблемы нет. Для контроля менял состояние выходов LPT порта и проверял при помощи осцилографа. Частоту держало, но периодически (предполагаю при свопе на диск) всё плыло. Реже на такую же проблему наблюдал даже на QNX 4.25, если на десктопном варианте с Фотоном. Хотя система реального времени. На одноплатниках с чистым QNX проблем вроде не было.

    Поэтому стоит разделять:

    • возможности самого таймера,

    • летенси обработки прерываний таймера и летенси добавляемый OS,

    • доступ к настройкам таймера в пользовательском API (на уровне драйверов он ограничен лишь самим аппаратным таймером)


    1. NTDLL
      14.02.2022 03:04

      Такую практику сейчас найти невозможно, вам здорово повезло


    1. Alexx999 Автор
      14.02.2022 03:16
      +1

      В целом это решение на жесткое реальное время не претендует - в случае повышенной нагрузки на систему оно уплывёт. У меня есть вариант посложнее с комленсациями но всё-таки Windows - не RTOS, а C# так и подавно, приходит Garbage Collection и можно гасить свет.


  1. vadimr
    14.02.2022 13:44

    Вы продолжительность промежутка для контроля своего метода меряете тем же инструментом, которым этот промежуток формируете. Так не пойдёт. Просим осциллограф.


    1. napa3um
      14.02.2022 14:31

      Пришёл написать этот комментарий. Да, гладкость графиков - это показатель упорядоченности квантов исполнения, а не их реальной равномерности в физическом времени. Нужно замерять внешним инсртументом, и очень может оказаться, что ограничили точность таймеров разработчики винды вовсе не из жадности (и не из жадности всякие конторы торгуют реалтаймовыми ядрами осей) :).


      1. Alexx999 Автор
        14.02.2022 15:01

        Разница в том что реалтаймовое ядро дает гарантии а тут... "как получится".

        Измерить извне я могу попробовать но мне банально некуда воткнуть щуп осцилографа - нужна железка, быстрая, без буфера, без регистрации и без смс. Разве что взять USB-COM адаптер и попробовать на нём сгенерировать 500Гц меандр и посмотреть что с этого получится


        1. vadimr
          14.02.2022 15:05

          Ну да, хотя бы меандром 500 Гц. Я практически уверен, что там будут пропущенные периоды в изрядном количестве.


    1. Alexx999 Автор
      14.02.2022 14:34
      +1

      Замечание верное, но, откровенно говоря, для промежутков в 1мс (или даже 0.1мс) у современных машин на Windows средства измерения с огромнейшим избытком - измерить (и проконтролировать) можно, а вот уснуть на малый промежуток - нечем. Собственно, к решению задачи изначально подход с предположением что хардварный счетчик который используется ОС имеет запас в несколько порядков, конкретно на моей машине Windows репортит точность в 100ns и дальше исходим из доверия к этому счетчику.


      1. vadimr
        14.02.2022 14:58
        +1

        Пока значение хардварного счётчика дойдёт до обработки в вашей программе, много времени может пройти. Вы никак этого не проверите и не учтёте при такой методике. Вы меряете интервал между двумя событиями в вашем потоке исполнения.


  1. Xadok
    14.02.2022 14:36

    Не совсем понял, будет ли это рабочим решением в win 7 или только сильно позднее?


    1. Alexx999 Автор
      14.02.2022 14:56

      У нас вроде как работает уже пару лет, у клиентов есть Win7 машины. Но конкретно таймер в 1мс без гарантий - у нас этот же код крутит 10мс таймер повышенной точности.


      1. Xadok
        14.02.2022 15:08

        10 и 15 не столь большая разница все-таки, а вот если выставить 1мс локально, а окажется, что оно глобально утекло…