Авторские статьи Точное время: измеряем, применяем

Discussion in 'Статьи' started by begin_end, 14 Nov 2009.

  1. begin_end

    begin_end

    Joined:
    4 Jan 2007
    Messages:
    252
    Likes Received:
    557
    Reputations:
    470
    Цель данной статьи – изложить полученный в ходе работы над проблемой материал о способах максимально точного измерения времени и использования на практике этих способов, а также рассмотреть варианты управления чем-либо программным с максимально достижимой точностью.

    Статья рассчитана на читателей, уже имеющих некоторый опыт в программировании, и заметивших проблему точности выдержки временных интервалов стандартных функций. Автор статьи, begin_end, советует её читателям, программирующим на языке Delphi, так как все способы реализуются именно на этом языке.

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

    Программист, который уже пробовал разрабатывать различные прикладные приложения, например, связанные с передачей данных или с генерацией/анализом сигналов мог заметить, что все стандартные функции (sleep, beep, GetTickCount, таймеры) обладают большой погрешностью при работе с малыми значениями временного интервала. Это определено разрешением системного таймера, значение которого для разных компьютеров может несколько различаться. Узнать это разрешение можно, используя функцию GetSystemTimeAdjustment:

    Code:
    BOOL GetSystemTimeAdjustment(
        PDWORD lpTimeAdjustment,	// size, in 100-nanosecond units, of a periodic time adjustment
        PDWORD lpTimeIncrement,	// time, in 100-nanosecond units, between periodic time adjustments
        PBOOL lpTimeAdjustmentDisabled	// whether periodic time adjustment is disabled or enabled
       );
    Разберем эту функцию для использования в Delphi. В lpTimeIncrement записывается значение разрешения системного таймера в единицах по 100 наносекунд. Нам нужно получить это значение, и вывести его, к примеру, в миллисекундах. Получится такая программка (см. пример 1):

    Code:
    program SysTmrCycle;
    
    {$APPTYPE CONSOLE}
    
    uses
      SysUtils, windows;
    
      var a,b:DWORD; c:bool;
    begin
      GetSystemTimeAdjustment(a,b,c);
      WriteLn('System time adjustment: '+FloatToStr(b / 10000)+' ms.');
      WriteLn;
      Writeln('Press any key for an exit...');
      Readln;
    end.
    Результат исполнения выводится на экран, у меня значение таймера оказалось равным 10,0144 миллисекунд.

    Что реально означает эта величина? То, что временные интервалы функций будут практически всегда кратны этой величине. Если это 10,0144 мс, то функция sleep(1000) вызовет задержку в 1001,44 мс. При вызове же sleep(5) задержка будет примерно 10 мс. Стандартный таймер Delphi, объект TTimer, естественно подвержен погрешности, но в еще большей степени. Объект TTimer основан на обычном таймере Windows, и посылает окну сообщения WM_TIMER, которые не являются асинхронными. Эти сообщения ставятся в обычную очередь сообщений приложения и обрабатываются, как и все остальные. Кроме того, WM_TIMER обладает самым низким приоритетом (исключая WM_PAINT), по отношению к другим сообщениям. GetMessage отправляет на обработку сообщение WM_TIMER лишь тогда, когда приоритетных сообщений в очереди больше не остается – сообщения WM_TIMER могут задерживаться на значительное время. Если время задержки превышает интервал, то сообщения объединяются вместе, таким образом, происходит еще и их утрата [1].
    Для того чтобы хоть как то производить замеры для сравнительного анализа функций задержки, необходим инструмент, позволяющий точно измерять временные интервалы выполнения некоторого участка кода. GetTickCount не подойдет ввиду вышеописанного. Но автор узнал об возможности опираться на частоту тактов процессора, за некоторый интервал времени. Начиная с Pentium III, процессоры обычно содержат достаточно доступный программистам счетчик меток реального времени, Time Stamp Counter, TSC, представляющий собой регистр на 64 разряда, содержимое которого с каждым тактом процессора инкрементируется [2]. Счет в счетчике начинается с нуля каждый раз при старте (или аппаратном сбросе) ЭВМ. Получить значение счетчика в Delphi можно следующим образом (см. пример 2):

    Code:
    program rdtsc_view;
    
    {$APPTYPE CONSOLE}
    
    uses
      SysUtils, windows;
    
    function tsc: Int64;
    var ts: record
     case byte of
      1: (count: Int64);
      2: (b, a: cardinal);
     end;
    begin
     asm
      db $F;
      db $31;
      mov  [ts.a], edx
      mov  [ts.b], eax
     end;
     tsc:=ts.count;
    end;
    
    begin
     repeat WriteLn(FloatToStr(tsc)) until false;
    end.
    Здесь ассемблерная вставка помещает результат счетчика в регистры edx и eax, значение которых затем переносится в ts, откуда доступно как ts.count типа Int64. Приведенная программа непрерывно выводит в консоль значения счетчика. На некоторых версиях Delphi есть готовая команда rdtsc (read time stamp counter), позволяющая сразу получить значение счетчика функцией RDTSC [3] вот так:

    Code:
    function RDTSC: Int64; register;
    asm
     rdtsc
    end;
    Предположим, у нас есть значение счетчика, но как использовать его? Очень просто. Опираясь на то, что значение изменяется с постоянной частотой можно вычислять разницу в количестве тактов процессора после исследуемой команды и до нее:

    Code:
    a:=tsc;
    Command;
    b:=tsc-a;
    В b будет число тактов процессора, прошедшее за время исполнения Command. Но тут есть один момент. Вызов tsc, дающий нам число тактов сам должен тоже затрачивать на это какое то количество тактов. И, для верности результата, его нужно вносить, как поправку, вычитаемую из полученного количества тактов:

    Code:
    a:=tsc;
    C:=tsc-a;
    a:=tsc;
    Command;
    b:=tsc-a-C;
    Все бы ничего, но экспериментально получается, что иногда значения нашей поправки C различаются. Причина этого была найдена. Дело тут в особенности функционирования процессора, точнее его конвейера. Продвижение машинных инструкций по конвейеру связано с рядом принципиальных трудностей, в случае каждой из них конвейер простаивает. Время выполнения инструкции в самом лучшем случае определяется пропускной способностью конвейера. Промежуток времени, которому можно гарантированно верить, получая такты процессора – от 50 тактов [2]. Получается, что в случае определения поправки, самым точным значением будет минимальная величина. Экспериментально достаточно производить вызов функции поправки до 10 раз:

    Code:
    function calibrate_runtime:Int64;
    var i:byte; tstsc,tetsc,crtm:Int64;
    begin
     tstsc:=tsc;
     crtm:=tsc-tstsc;
     for i:=0 to 9 do
      begin
       tstsc:=tsc;
       crtm:=tsc-tstsc;
       if tetsc<crtm then crtm:=tetsc;
      end;
     calibrate_runtime:=crtm;
    end;
    Теперь, когда у нас есть необходимый инструмент, поэкспериментируем с функциями задержки. Начнем со всем известной и всеми применяемой sleep:

    Code:
    procedure Sleep(milliseconds: Cardinal); stdcall;
    Чтобы провести проверку точности задержки, включим в нашу консольную программу, кроме кода tsc и кода calibrate_runtime следующий код:

    Code:
    function cycleperms(pau_dur:cardinal):Int64;
    var tstsc,tetsc:Int64;
    begin
     tstsc:=tsc;
     sleep(pau_dur);
     tetsc:=tsc-tstsc;
     cycleperms:=(tetsc-calibrate_runtime) div pau_dur;
    end;
    Этот код мы вызовем из программы, задавая по нескольку раз разные значения pau_dur (паузы).Если вы обратили внимание, число тактов за время паузы затем делится на значение паузы. Так мы узнаем точность задержки в зависимости от ее времени. Для удобства проведения теста и вывода на экран/сохранения результата теста применен такой код (см. пример 3):

    Code:
    var test_result,temp_result:string; n:cardinal; i:byte; aver,t_res:Int64; res:TextFile;
    begin
     WriteLn('The program will generate a file containing the table of results of measurements of quantity of cycles of the processor in a millisecond. Time of measurement is chosen'+' miscellaneous, intervals: 1, 10, 100, 1000, 10000 ms. You will see distinctions of measurements. If an interval of measurement longer - results will be more exact.');
     WriteLn;
     Writeln('Expected time of check - 1 minute. Press any key for start of the test...');
     ReadLn;
     temp_result:='Delay :'+#9+'Test 1:'+#9+'Test 2:'+#9+'Test 3:'+#9+'Test 4:'+#9+'Test 5:'+#9+'Average:';
     n:=1;
     test_result:=temp_result;
     WriteLn(test_result);
     while n<=10000 do
      begin
       temp_result:=IntToStr(n)+'ms'+#9;
       aver:=0;
       for i:=1 to 5 do
        begin
         t_res:=cycleperms(n);
         aver:=aver+t_res;
         temp_result:=temp_result+IntToStr(t_res)+#9;
        end;
       WriteLn(temp_result+IntToStr(aver div 5));
       test_result:=test_result+#13+#10+temp_result+IntToStr(aver div 5);
       n:=n*10;
      end;
     WriteLn;
     AssignFile(res,'TCC_DEF.xls');
     ReWrite(res);
     Write(res,test_result);
     CloseFile(res);
     WriteLn('The test is completed. The data are saved in a file TCC_DEF.xls.');
     Writeln('Press any key for an exit...');
     ReadLn;
    end.
    В нем мы исполняем cycleperms по пять раз для каждого временного интервала (от 1 до 10000 миллисекунд), а также считаем среднее значение. Получается таблица. Итак, полученные числа тактов процессора в ходе такого исследования:

    [​IMG]

    Картину мы наблюдаем не самую лучшую. Поскольку частота процессора примерно 1778,8 МГц (см. пример 4), то значения тактов за 1 миллисекунду должны стремиться к приблизительному числу 1778800. Точность функции sleep не дает нам этого ни за 1, 10, 100 или 1000 миллисекунд. Только за десятисекундный промежуток времени значения близки. Пожалуй, если бы в тесте 4 не было 1781146, то усредненная величина была бы приемлемой.

    Что можно сделать? Оставить функцию и рассмотреть что-то еще? Пока не стоит торопиться. Я узнал, что можно вручную задавать погрешность отсчета эталонного интервала времени, используя функцию timeBeginPeriod [2]:

    Code:
    MMRESULT timeBeginPeriod(
        UINT uPeriod	
       );
    Для поддержания такого высокоточного разрешения используются дополнительные системные ресурсы, поэтому нужно вызывать timeEndPeriod для их высвобождения по завершению всех операций. Код функции cycleperms для исследования такого sleep (см. пример 5):

    Code:
    function cycleperms(pau_dur:cardinal):Int64;
    var tstsc,tetsc:Int64;
    begin
     timeBeginPeriod(1);
     sleep(10);
     tstsc:=tsc;
     sleep(pau_dur);
     tetsc:=tsc-tstsc;
     timeEndPeriod(1);
     cycleperms:=(tetsc-calibrate_runtime) div pau_dur;
    end;
    Еще есть малообъяснимая особенность, timeBeginPeriod(1), устанавливающая разрешение в 1 миллисекунду начинает давать эффект не сразу, а только после вызова sleep, поэтому в код, после timeBeginPeriod вставлено sleep(10). Результаты этого исследования:

    [​IMG]

    Наблюдаемые данные гораздо лучше. Среднее значение за 10 секунд довольно точно. Среднее за 1 миллисекунду отличается от него всего на 1,7 %. Соответственно отличия за 10 мс составляет 0,056 %, за 100 мс – 0,33 % (странно вышло), за 1000 мс – 0,01 %. Меньший, чем 1 мс интервал, невозможно использовать в sleep. Но можно твердо сказать, что sleep годна для пауз в 1 мс при условии выполнения timeBeginPeriod(1), и точность sleep только растет с ростом задаваемого временного промежутка (см. пример 6).
     
    _________________________
    11 people like this.
  2. begin_end

    begin_end

    Joined:
    4 Jan 2007
    Messages:
    252
    Likes Received:
    557
    Reputations:
    470
    Функция sleep основана на Native API функции NtDelayExecution, которая имеет следующий вид [5]:

    Code:
    NtDelayExecution(
      IN BOOLEAN              Alertable,
      IN PLARGE_INTEGER       DelayInterval );
    Попробуем провести тест ее задержек, подобно sleep, но учитывать будет она даже микросекунды:

    Code:
    function cyclepermks(pau_dur:Int64):Int64;
    var tstsc,tetsc,p:Int64;
    begin
     p:=-10*pau_dur;
     tstsc:=tsc;
     NtDelayExecution(false,@p);
     tetsc:=tsc-tstsc;
     cyclepermks:=(tetsc-calibrate_runtime) *1000 div pau_dur;
    end;
    Эта функция не прописана в windows.pas или ином другом файле, потому вызовем ее, добавив строку:

    Code:
    procedure NtDelayExecution(Alertable:boolean;Interval:PInt64); stdcall; external 'ntdll.dll';
    Код, в котором мы вызываем функцию и строим таблицу результатов, следует подкорректировать вот так (см. пример 7):

    Code:
    var test_result,temp_result:string; n:Int64; i:byte; aver,t_res:Int64; res:TextFile;
    begin
     WriteLn('The program will generate a file containing the table of results of measurements of quantity of cycles of the processor in a mikrosecond. Time of measurement is chosen'+' miscellaneous, intervals: 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000 mks. You will see distinctions of measurements. If an interval of measurement longer - results will be more exact.');
     WriteLn;
     Writeln('Expected time of check - 1 minute. Press any key for start of the test...');
     temp_result:='Delay :'+#9+'Test 1:'+#9+'Test 2:'+#9+'Test 3:'+#9+'Test 4:'+#9+'Test 5:'+#9+'Average:';
     n:=1;
     test_result:=temp_result;
     WriteLn(test_result);
     while n<=10000000 do
      begin
       temp_result:='10^'+IntToStr(length(IntToStr(n))-1)+'mks'+#9;
       aver:=0;
       for i:=1 to 5 do
        begin
         t_res:=cyclepermks(n);
         aver:=aver+t_res;
         temp_result:=temp_result+IntToStr(t_res)+#9;
        end;
       WriteLn(temp_result+IntToStr(aver div 5));
       test_result:=test_result+#13+#10+temp_result+IntToStr(aver div 5);
       n:=n*10;
      end;
     WriteLn;
     AssignFile(res,'TCC_NTAPI.xls');
     ReWrite(res);
     Write(res,test_result);
     CloseFile(res);
     WriteLn('The test is completed. The data are saved in a file TCC_NTAPI.xls.');
     Writeln('Press any key for an exit...');
     ReadLn;
    end.
    После проведения исследования задержек, создаваемых NtDelayExecution получились интересные результаты:

    [​IMG]

    Видно, что применять такую точность этой функции бесполезно на промежутках менее 1 миллисекунды. Прочие интервалы задержек несколько лучше, чем у sleep без измененного разрешения, но хуже, чем с высоким разрешением sleep (в принципе это понятно, ведь тут мы не создавали потоков с повышенным приоритетом, и вообще не делали ничего для повышения точности, подобно тому, как это делает timeBeginPeriod). А если добавить timeBeginPeriod? Посмотрим, что получится:

    [​IMG]

    На микросекундных интервалах ситуация все та же. А вот на интервалах, начиная с 1 миллисекунды отличие, относительно 10-секундного значения составляет 0,84 %, что лучше аналогичного использования sleep (1,7 %) – NtDelayExecution дает задержку точнее.
    При поиске средств программирования задержек в исполнении кода был найден еще один вариант [4], вроде бы предоставляющий возможность указывать интервал в микросекундах. Это WaitableTimer. Работать с ним можно через функции CreateWaitableTimer, SetWaitableTimer, WaitForSingleObjectEx. Вид процедуры cyclepermks, куда мы добавили WaitableTimer:

    Code:
    function cyclepermks(pau_dur:Int64):Int64;
    var tstsc,tetsc,p:Int64; tmr:cardinal;
    begin
     tmr:=CreateWaitableTimer(nil, false, nil);
     p:=-10*pau_dur;
     tstsc:=tsc;
     SetWaitableTimer(tmr, p, 0, nil, nil, false);
     WaitForSingleObjectEx(tmr, infinite, true);
     CloseHandle(tmr);
     tetsc:=tsc-tstsc;
     cyclepermks:=(tetsc-calibrate_runtime2) *1000 div pau_dur;
    end;
    Особенность применения WaitableTimer требует от нас также модификации расчета поправки, получаемой в calibrate_runtime:

    Code:
    function calibrate_runtime2:Int64;
    var i:byte; tstsc,tetsc,crtm, p:Int64; tmr:cardinal;
    begin
     tstsc:=tsc;
     crtm:=tsc-tstsc;
     for i:=0 to 9 do
      begin
       tmr:=CreateWaitableTimer(nil, false, nil);
       p:=0;
       tstsc:=tsc;
       SetWaitableTimer(tmr, p, 0, nil, nil, false);
       CloseHandle(tmr);
       crtm:=tsc-tstsc;
       if tetsc<crtm then crtm:=tetsc;
      end;
     calibrate_runtime2:=crtm;
    end;
    Ведь SetWaitableTimer и CloseHandle тоже исполняются за период учитываемого нами количества тактов процессора. Сразу добавим в код cyclepermks вызов timeBeginPeriod, надеясь на помощь этой процедуры в приросте точности (см. пример 8). Таблица результатов:

    [​IMG]

    Увы, и здесь мы не получили возможность устанавливать задержки для промежутков меньше миллисекундных. Разница значений 1 миллисекунды и 10 секунд равна 5 %. В сравнении с предыдущими способами, это хуже.

    Перед тем, как делать выводы, скажу немного о собственно самом измерении времени. В приведенных исследованиях основой сравнений было число тактов процессора и у каждого компьютера оно разное. Если понадобится привести его к единицам времени на основе секунд, то нужно сделать следующее: применяя 10-секундную задержку NtDelayExecution получить число тактов процессора за эти 10 секунд или узнать длительность одного такта (см. пример 9). Зная количество тактов процессора в единицу времени, можно спокойно преобразовывать меньшие значения числа тактов процессора в значения времени. Кроме этого рекомендуется установить приложению приоритет реального времени.

    Заключение. В результате проведенной работы было установлено, что можно очень точно (даже до отрезка времени, исчисляемого 50 тактами процессора) замерять время на ЭВМ. Эта задача решена успешно. Что же касается возможности самостоятельно задавать точные задержки в исполняемом коде, то тут ситуация такова: лучший обнаруженный метод, позволяет сделать это с разрешением не большим, чем 1 миллисекунда, с погрешностью разрешения на интервале 1 мс порядка 0,84 %. Это функция NtDelayExecution с установкой разрешения процедурой timeBeginInterval. Недостаток функции, по сравнению с оказавшейся менее точной sleep это громоздкий вызов и нахождение в составе недостаточно документированного Native API. Использовать Native API не советуют по причине возможной несовместимости отдельных API в разных операционных системах семейства Windows. В общем, то, очевидное преимущество функции NtDelayExecution все-таки вынуждает сделать выбор в ее пользу.

    Примеры:
    1. Определение разрешения системного таймера
    2. Вывод RDTSC
    3. Задаем интервал через sleep
    4. Узнаем частоту процессора
    5. Задаем интервал через sleep более точно
    6. Исследуем точность установки интервала через sleep на разных значениях
    7. Интервал с помощью NtDelayExecution
    8. Интервал посредством WaitableTimer
    9. Узнаем длительность одного процессорного такта
    Примеры содержат файлы *.dpr исходного кода (на языке Delphi), скомпилированное консольное *.exe приложение и (некоторые) *.xls таблицу уже полученных автором результатов (в формате, поддерживаемом MS Excel). Все примеры – одним файлом.

    Литература:
    1. Руссинович М., Соломон Д. Внутреннее устройство Microsoft Windows. – СПб.: Питер, 2005. – 992 с.
    2. Щупак Ю.А. Win32 API. Эффективная разработка приложений. – СПб.: Питер, 2007. – 572 с.
    3. RDTSC – Wikipedia [http://ru.wikipedia.org/wiki/Rdtsc]
    4. CreateWaitableTimer – MSDN [http://msdn.microsoft.com/en-us/library/ms682492(VS.85).aspx]
    5. NtDelayExecution – RealCoding [http://forums.realcoding.net/lofiversion/index.php/t16146.html]


    Статья была написана 13.11.2009, автор begin_end. Некоторые моменты, рассматриваемые в статье автор обсуждал со slesh’ем, которому выражается благодарность за такую помощь.
     
    _________________________
    2 people like this.
  3. Root-access

    Root-access Elder - Старейшина

    Joined:
    18 Jun 2008
    Messages:
    194
    Likes Received:
    195
    Reputations:
    91
    Интересный материал.
    Вопрос: а как насчёт аналогичных функций в других языках? Как там дела с погрешностями?
     
  4. begin_end

    begin_end

    Joined:
    4 Jan 2007
    Messages:
    252
    Likes Received:
    557
    Reputations:
    470
    Я думаю, там будет аналогичная ситуация.
    Для C++ можно взять попробовать реализовать примеры из 10 главы книги Ю.А.Щупака " Win32 API. Эффективная разработка приложений".

    Там рассматривают для определения точных интервалов времени как RTDST метод, так и функции QueryPerfomanceCounter (я ее не использовал, т.к. она все равно основана на работе с RTDST).

    Кроме того там есть готовые примеры собственных таймеров для программирования задержек, однако, у них все равно недостаточное разрешение (до 1 мс), выше которого обычными способами в Windows достичь невозможно.
     
    _________________________
  5. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    1,995
    Likes Received:
    4,767
    Reputations:
    693
    begin_end, подскажите, как можно использовать код (с таймером который) из примера в статье для синхронизации действий флудботов, например, из статьи https://forum.antichat.ru/threadnav47764-2-10.html ?
     
  6. begin_end

    begin_end

    Joined:
    4 Jan 2007
    Messages:
    252
    Likes Received:
    557
    Reputations:
    470
    В основном коде бота сделать таймер:
    Code:
     sinhro:=CreateWaitableTimer(nil, false, nil); //создание таймера
     pause:=-10000000; //это интервал в 1 секунду
     SetWaitableTimer(sinhro, pause, 0, nil, nil, false); //установка таймера
    А в каждом из потоков-флудботов ждать сообщения таймера с помощью:
    Code:
    WaitForSingleObjectEx(sinhro, infinite, true);
    Не забываем делать CloseHandle(sinhro), когда таймер не нужен.
     
    _________________________
    1 person likes this.
  7. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,704
    Likes Received:
    1,224
    Reputations:
    455
    Стабильность микропаузы

    Небольшое дополнение статейки от меня )

    Цель:
    Проверить стабильность паузы на интервалах сравнимых с микросекундами. Да и вообще возможность задания стабильных пауз данной длительности

    Инструменты:
    1) Delphi
    2) 7z / WinRAR + фильм на 2 гига.
    3) Процессор Intel Dual Core E5200 ~ 2.5 Ггц
    4) Win XP SP 3
    Остальное не играет значительной роли.

    Метод проверки(Теоретическая часть):
    Как всем известно, реальной многопоточного выполнения кода в процах (Intel/ADM) практически нет (исключение - многоядерные и/или многопоточные (Hyper Threading)) GPU учитывать не будем, потому как они не стоят как основа системы.
    В виду вышесказанного для реализации многопоточности приходится использовать прерывание выполнения одного потока и передачу управления другому. Этим самым достигается эффект параллельного выполнения кода. т.е. пауза настолько мала, что этого практически не видно при обычном использовании компьютера. По этому было введено такое понятие как - процессорное время выделяемое потоку. Когда оно заканчивается, то управление получает другой поток системы и так пока все потоки не получат управление.
    Так что теоретически пауза которая будет во время пока поток не получит сново управление, может быть примерно составлять произведение кол-ва поток в системе, на процессорное время каждого из них.

    Практически это значение будет разным, т.к. зависит от приоритета потока, состояния потока (активный/неактивны), а также от загруженности процессора.

    Хоть это время и мало, но на малых интервалах времени это уже будет заметно.

    Так что основная задача будет - вычислить примерное значение этой паузы при разных условиях и сравнить её длительность с 1-10 микросекундами


    Метод проверки(Алгоритмическая часть):
    Т.к. значения очень малы и GetTickCount на не сможет жать настолько точные данные, то придется делать вычисления в самых точных единицах - тактах процессора, т.к. их кол-во легко и быстро получается и оно довольно стабильно.

    Алгоритм действия будет таков:
    0) установить нужный приоритет потока
    1) засечь текущее кол-во тактов
    2) сохранить это значение в массиве
    3) выполнить действия минимально использующие ресурсы системы за исключением процессора. работа с памятью и процессором (без математических вычислений). В нашем случае это будет сохранение в массиве
    3) сново получить кол-во тактов
    4) сохранить это значение в массиве
    5) проделать большое кол-во раз операции 1 - 4
    Это будет сделано для того, чтобы точно попасть на время когда закончится процессорное время патока.
    6) вернуть приоритет
    7) пройтись по массиву и вычислить минимум и максимум.
    Максимум - наибольшая время выполнение команды (в тактах)
    Минимум - реальное время выполнения команды

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

    разница между максимум и минимум как раз и будет время паузы в тактах.

    Метод проверки(Практическая часть часть):
    Code:
    var
      x : array [0..100000] of int64; // массивы для хранения
      y : array [0..100000] of int64; // кол-ва таквто
    
    // массив приоритетов 
     TestPrior : array[0..3] of integer = (THREAD_PRIORITY_LOWEST, THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_HIGHEST, THREAD_PRIORITY_TIME_CRITICAL);
    // массив названий приоритета
     TestName : array[0..3] of string = ('Низкий', 'Средний', 'Высокий', 'Очень высокий');
    
    // процедура проверки
    // max_time будет хранить максимум
    // min_time будет хранить минимум
    procedure GetMaxMinByPrior(var max_time : int64; var min_time : int64; prior : integer);stdcall;
    var
      i : integer;
      Thread : Cardinal;
    begin
      Thread := GetCurrentThread;
      SetThreadPriority(Thread, TestPrior[prior]); // установим потоку нужный приоритет
    
      Sleep(100); // сделаем паузу чтобы приоритет вступил в действия наверняка. 
    
      asm
        lea esi, x // начало массива
        lea edi, y// начало массива
    
        mov ecx, 100000 // 100k раз повторим замеры
        @m1:
        rdtsc // получим кол-во тактов
        mov dword [esi], eax // сохраним младшую часть
        mov dword [esi+4], edx // сохраним старшую часть
    
        rdtsc // опять получим кол-во тактов
       // опять сохраним
        mov dword [edi], eax 
        mov dword [edi+4], edx
    
        add esi, 8 // перейдем на следующий элемент массива
        add edi, 8
        loop @m1 // повторим цикл
      end;
      // вернем обратно приоритет
      SetThreadPriority(Thread, THREAD_PRIORITY_NORMAL);
      max_time := 0;
      min_time := $FFFFFFFFFFFF;
    
      for i := 0 to 99999 do // пройдемся по элементам
      begin
        // будим искать максимум
        if max_time < y[i] - x[i] then max_time := y[i] - x[i];
       // будим искать минимум
        if min_time > y[i] - x[i] then min_time := y[i] - x[i];
      end;
    
    end;
    
    
    procedure TForm1.Button1Click(Sender: TObject);
    const
      N = 10; // 10 раз будем повторять один и тотже тест
    
    var
      max : int64;
      min : int64;
      x : integer;
      y : integer;
      smax, smin : int64;
    begin
      for y := 0 to 3 do // выполним все тесты
      begin
        memo1.Lines.Add('---------- '+TestName[y]+' ----------');
        smax := 0;
        smin := $FFFFFFFFFFF;
    
        for x := 1 to N do // повторы выполнения теста
        begin
          GetMaxMinByPrior(max, min, y);
          // сразу найдем максимум и минимум среди повторов
          if smax < max then smax := max;
          if smin > min then smin := min;
        end;
        // выведем данные
        memo1.Lines.Add(inttostr(smax) + #9 + inttostr(smin))
      end;
    end;
    
    Анализ полученных данных:
    1) При простое процессора
    Низкий 76825 25
    Средний 29575 25
    Высокий 19275 25
    Очень высокий 23188 25

    2) При загруженности процессора упаковкой фильма через winrar
    Низкий 143088 25
    Средний 858150 25
    Высокий 53737 25
    Очень высокий 23387 25

    3) Тестирование упаковке через 7z (с поддержкой многоядерных систем). Тест для того, чтобы загрузить оба ядра сразу.

    Низкий 2092750 25
    Средний 3358850 25
    Высокий 80412 25
    Очень высокий 35487 25

    Из этого выводы такие:
    1) приоритет играет малую роль на паузу, при выполнение небольшого кол-ва данных. По этому пауза может быть на высоком приоритете больше чем на низком.
    2) загруженность процессора довольно хорошо влияет.
    3) минимальное значение за которое может процессор выполнить команды - 25 тактов.
    4) Также зависит от работы планировщика задач. т.е. при некоторых тестах иногда вылазели данные 10458800, что соответсовало бы 4 микросекундам


    И так, ради чего это проверяли:
    Будет брать паузу только на нормальном приоритете
    29550 тактов (без загруженности )
    858125 тактов (с загруженностью 1 ядра)
    3358825 тактов (с загруженность 2 ядер)
    Если учесть что процессор имеет частоту 2,5 Ггц
    то это 2,5 миллиарда тактов в секунду.
    Или же 2,5 миллиона тактов в микросекунду.
    Или же 2500 тактов в наносекунду.
    Из полученных банных можно говорить что
    1) при простое выходит 0,011 микросекунды пауза
    2) при загруженности 1 ядра выходит 0,343 микросекунды пауза
    3) при загруженности 2 ядер 1,3 микросекунды

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

    Также можно было заметить что при загруженности 2-х ядер сразу пауза вышла за пределы 1 микросекунды.
    Для одноядерных процессоров результат будет еще хуже.

    Вот и всё
    (С) SLESH
     
    #7 slesh, 17 Nov 2009
    Last edited: 17 Nov 2009
    2 people like this.
  8. begin_end

    begin_end

    Joined:
    4 Jan 2007
    Messages:
    252
    Likes Received:
    557
    Reputations:
    470
    slesh, опробовал твой тест на компьютере с CPU = 334,1 МГц (Pentium II), RAM = 128MB.

    Результаты при отсутствии существенной нагрузки на процессор:
    Результаты при загруженности процессора 100% (упаковка архиватором):
    Интересные данные...
     
    _________________________
  9. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,704
    Likes Received:
    1,224
    Reputations:
    455
    Как можеш видеть - менеджер потоков ведет себя неадекватно
    Без нагрузки - Высокий 101644 33
    с нагрузкой - Высокий 88359 33