Antichat снова доступен.
Форум Antichat (Античат) возвращается и снова открыт для пользователей.
Здесь обсуждаются безопасность, программирование, технологии и многое другое.
Сообщество снова собирается вместе.
Новый адрес: forum.antichat.xyz
Точное время: измеряем, применяем |

14.11.2009, 13:40
|
|
Members of Antichat - Level 5
Регистрация: 04.01.2007
Сообщений: 176
Провел на форуме: 17964969
Репутация:
1362
|
|
Точное время: измеряем, применяем
Цель данной статьи – изложить полученный в ходе работы над проблемой материал о способах максимально точного измерения времени и использования на практике этих способов, а также рассмотреть варианты управления чем-либо программным с максимально достижимой точностью.
Статья рассчитана на читателей, уже имеющих некоторый опыт в программировании, и заметивших проблему точности выдержки временных интервалов стандартных функций. Автор статьи, begin_end, советует её читателям, программирующим на языке Delphi, так как все способы реализуются именно на этом языке.
Наша задача – найти лучший метод точного измерения малых временных интервалов (желаемая точность – 10^-6 секунды), определить наиболее эффективный способ программирования задержек в исполнении кода, с такой же точностью.
Программист, который уже пробовал разрабатывать различные прикладные приложения, например, связанные с передачей данных или с генерацией/анализом сигналов мог заметить, что все стандартные функции (sleep, beep, GetTickCount, таймеры) обладают большой погрешностью при работе с малыми значениями временного интервала. Это определено разрешением системного таймера, значение которого для разных компьютеров может несколько различаться. Узнать это разрешение можно, используя функцию GetSystemTimeAdjustment:
Код:
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):
Код:
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):
Код:
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] вот так:
Код:
function RDTSC: Int64; register;
asm
rdtsc
end;
Предположим, у нас есть значение счетчика, но как использовать его? Очень просто. Опираясь на то, что значение изменяется с постоянной частотой можно вычислять разницу в количестве тактов процессора после исследуемой команды и до нее:
Код:
a:=tsc;
Command;
b:=tsc-a;
В b будет число тактов процессора, прошедшее за время исполнения Command. Но тут есть один момент. Вызов tsc, дающий нам число тактов сам должен тоже затрачивать на это какое то количество тактов. И, для верности результата, его нужно вносить, как поправку, вычитаемую из полученного количества тактов:
Код:
a:=tsc;
C:=tsc-a;
a:=tsc;
Command;
b:=tsc-a-C;
Все бы ничего, но экспериментально получается, что иногда значения нашей поправки C различаются. Причина этого была найдена. Дело тут в особенности функционирования процессора, точнее его конвейера. Продвижение машинных инструкций по конвейеру связано с рядом принципиальных трудностей, в случае каждой из них конвейер простаивает. Время выполнения инструкции в самом лучшем случае определяется пропускной способностью конвейера. Промежуток времени, которому можно гарантированно верить, получая такты процессора – от 50 тактов [2]. Получается, что в случае определения поправки, самым точным значением будет минимальная величина. Экспериментально достаточно производить вызов функции поправки до 10 раз:
Код:
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:
Код:
procedure Sleep(milliseconds: Cardinal); stdcall;
Чтобы провести проверку точности задержки, включим в нашу консольную программу, кроме кода tsc и кода calibrate_runtime следующий код:
Код:
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):
Код:
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 миллисекунд), а также считаем среднее значение. Получается таблица. Итак, полученные числа тактов процессора в ходе такого исследования:
Картину мы наблюдаем не самую лучшую. Поскольку частота процессора примерно 1778,8 МГц (см. пример 4), то значения тактов за 1 миллисекунду должны стремиться к приблизительному числу 1778800. Точность функции sleep не дает нам этого ни за 1, 10, 100 или 1000 миллисекунд. Только за десятисекундный промежуток времени значения близки. Пожалуй, если бы в тесте 4 не было 1781146, то усредненная величина была бы приемлемой.
Что можно сделать? Оставить функцию и рассмотреть что-то еще? Пока не стоит торопиться. Я узнал, что можно вручную задавать погрешность отсчета эталонного интервала времени, используя функцию timeBeginPeriod [2]:
Код:
MMRESULT timeBeginPeriod(
UINT uPeriod
);
Для поддержания такого высокоточного разрешения используются дополнительные системные ресурсы, поэтому нужно вызывать timeEndPeriod для их высвобождения по завершению всех операций. Код функции cycleperms для исследования такого sleep (см. пример 5):
Код:
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). Результаты этого исследования:
Наблюдаемые данные гораздо лучше. Среднее значение за 10 секунд довольно точно. Среднее за 1 миллисекунду отличается от него всего на 1,7 %. Соответственно отличия за 10 мс составляет 0,056 %, за 100 мс – 0,33 % (странно вышло), за 1000 мс – 0,01 %. Меньший, чем 1 мс интервал, невозможно использовать в sleep. Но можно твердо сказать, что sleep годна для пауз в 1 мс при условии выполнения timeBeginPeriod(1), и точность sleep только растет с ростом задаваемого временного промежутка (см. пример 6).
|
|
|
|
|
Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
|
|
|
|