Показать сообщение отдельно

  #2  
Старый 14.11.2009, 13:41
begin_end
Участник форума
Регистрация: 04.01.2007
Сообщений: 176
Провел на форуме:
17964969

Репутация: 1362
Post

Функция sleep основана на Native API функции NtDelayExecution, которая имеет следующий вид [5]:

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

Код:
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 или ином другом файле, потому вызовем ее, добавив строку:

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

Код:
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 получились интересные результаты:


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


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

Код:
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:

Код:
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). Таблица результатов:


Увы, и здесь мы не получили возможность устанавливать задержки для промежутков меньше миллисекундных. Разница значений 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/libr...92(VS.85).aspx]
5. NtDelayExecution – RealCoding [http://forums.realcoding.net/lofiver...hp/t16146.html]


Статья была написана 13.11.2009, автор begin_end. Некоторые моменты, рассматриваемые в статье автор обсуждал со slesh’ем, которому выражается благодарность за такую помощь.