Delphi

индекс
64,75

Точное время: измеряем, применяем

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

   Статья рассчитана на читателей, уже имеющих некоторый опыт в программировании, и заметивших проблему точности выдержки временных интервалов стандартных функций. Автор статьи, 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 миллисекунд), а также считаем среднее значение. Получается таблица. Итак, полученные числа тактов процессора в ходе такого исследования:
TCC_DEF

   Картину мы наблюдаем не самую лучшую. Поскольку частота процессора  примерно 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). Результаты этого исследования:
TCC

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

   Функция 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 получились интересные результаты:
TCC_NTAPI

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

   На микросекундных интервалах ситуация все та же. А вот на интервалах, начиная с 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). Таблица результатов:
TCC_WFSO

   Увы, и здесь мы не получили возможность устанавливать задержки для промежутков меньше миллисекундных. Разница значений 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’ем, которому выражается благодарность за такую помощь.
+18
14 ноября 2009, 04:27
29

комментарии (64)

+7
gribozavr #
Почему не QueryPerformanceCounter()?
0
rsa2048 #
Beware of QueryPerformanceCounter()
www.virtualdub.org/blog/pivot/entry.php?id=106
0
gribozavr #
The TSC is generally readable via the RDTSC instruction from user mode, making it the fastest, easiest, and most precise time base available on modern machines.

Alas, it is rather unsafe to use. [...]
0
rsa2048 #
QueryPerformanceCounter counts elapsed time, not CPU cycles
blogs.msdn.com/oldnewthing/archive/2008/09/08/8931563.aspx
+3
begin_end #
На мой взгляд, само получение значения tsc затратит меньше процессорных тактов и данные получатся точнее (как вместо sleep я использовал NtDelayExecution. Еще мелочь, но QueryPerformanceCounter возвратил мне 0 на системе PIII с win2k, а tsc — нормальное значение.
Хотя, я не против, конечно QueryPerformanceCounter, QueryPerformanceFrequency также вполне можно использовать.

Я еще поэкспериментирую с применением этих функций.
–2
gvsmirnov #
Почему на ум приходят постоянная Планка и принцип неопределённости Гейзенберга?)
+2
denim #
пока у вашей операционной системы не появится real-time scheduler говорить о каких бы то ни было точных измерениях времени бесполезно… thread/process switch отжирает драгоценное время, обработка прерываний происходит в ущерб программам уровня пользователя. опираться на полученные результаты нет смысла. единственное, что гарантируют все эти функции — процесс проспит не меньше указанного интервала времени. под различной нагрузкой в реальных приложениях все это треснет по швам к сожалению
+2
sysprg #
Дополнительная информация:
1. На некоторых процессорах значение RDTSC может быть рассинхронизировано для разных ядер. Вы никогда не знаете, на каком именно ядре в данный момент выполняется Ваша программа (если только не выставили affinity mask, да и то, я бы не был на 100% уверен), поэтому чтение TSC процессорной командой иногда само вносит существенные искажения. В SMP системах RDTSC вообще по определению рассинхронизировано между процессорами. Поэтому думаю, что performance counters лучший метод для точных замеров времени в прикладных программах для Windows.
2. Программу для Windows не стоит писать так, чтобы она зависела от задержек в функциях типа Sleep. Если речь идет об управлении устройством в реальном времени, то нужно выносить все это в драйвер. В других случаях использовать мультимедийные функции для таймирования. А если кто-то пишет часы для desktop-а – вероятно лучше всего подойдут waitable timers.
0
brutaltag #
Используя для оптимизации кода профайлер из состава библиотеки Jedi, позволил себе исследовать его с целью определения не только общего времени выполнения блока кода, количества запусков и, соответственно, среднего времени операции, но и максимального времени выполнения одиночной операции. Обнаружилось нечто интересное — для блоков кода со средним временем выполнения менее 1 мс значение максимального времени давало часто 16 мс, а то и 32. Особенно когда общее время выполнения при сотнях и тысячах запусков как раз получается те же 16 или 32 мс.

В том профайлере для замера временных отсечек используется функция GetTickCount. Не подскажете, чем заменить ее для более точного отсчета малых интервалов?
0
begin_end #
denim, все так, но описанное хотя бы чуть-чуть поднимает и пробует решить проблему…

sysprg, к сожалению не было возможности проверить и убедиться в преимуществах QueryPerformanceCounter на многоядерных системах, но судя по сказанному Вами, его действительно лучше использовать. Прежде всего я искал способ получить задержки, и для эксперимента, в котором RDTSC служил для подсчета, он пока мне сгодился.
А задержки эти, увы с нужным интервалом не получил: тоже верно — нужен драйвер. Хотя если задержки до 1 мс, можно обойтись, как я описал (NtDelayExecution, sleep, таймеры).
0
sysprg #
Даже если выставить real-time приоритет для thread, то все равно из-за подкачки/откачки страниц или еще каких-то влияний ядро Windows не всегда мгновенно активирует пользовательскую программу, поэтому писать на прикладном уровне для Windows что-то зависящее от задержек в 1 миллисекунду и меньше — вообще неправильно и малореально. Как и для Linux. С такими вещами нужно или на уровне ядра работать, или использовать специализированные системы реального времени. Как минимум в Windows или в Linux нужно выключать своппинг и не запускать в параллель всякие мультимедийные программы. Но это уже превращает программу в пригодную к использованию только на выделенной машине. Вывод — не очень подходят ОС общего назначения для этого, на прикладном уровне…
+2
sysprg #
В любом случае спасибо за интересную статью! Можно убрать накопление погрешности, это да. Но нельзя обеспечить четкую активацию в точное время с точностью до миллисекунд. Если Вы запустите Ваши программы с любым методом точных задержек параллельно с какой-нибудь ресурсо-жрущей игрушкой, то Вы увидите, что какой бы Вы не использовали метод задержки и какой бы приоритет для thread не выбрали, но время от времени будут происходить задержки в активации thread более чем на одну миллисекунду из-за свопинга или из-за того, что некоторые directx операции не прерываются ядром, и обеспечить мгновенную активацию прикладной программы для Windows в суб-миллисекундном диапазоне в общем случае невозможно.
0
brutaltag #
Очень интересное исследование, спасибо! Срочно ищу по всем юнитам проекта, не используются ли где-либо малые задержки…
0
AnatolyB #
А не могли бы вы рассказать, для чего может потребоваться измерять временные интервалы с такой точностью?
0
AnatolyB #
Т.е. зачем это все нужно?
+1
infreerat #
профилирование программ

–1
AnatolyB #
Для профилирования достаточно отладочного режима, нет?
+2
infreerat #
всё зависит от конечной потребности;
иногда бывает что счёт идёт на такты, но rdtsc особо верить нельзя ± 50-100 тактов(pipe-line, out-of-order execution);
но да почти всегда достаточно и QueryPreformanceCounter/QueryPerformanceFrequency, которые работают через ZwQueryPerformanceCounter;
0
side2k #
Далеко не все промышленные системы являются «тру» real-time.
Однако сертификацию им проходить все равно требуется.
И это лишь один из примеров.
–1
AnatolyB #
Не повсем понял, поясните, что имеется ввиду.

Временные задержки при работе ПО непредсказуемы в обычной ОС непредсказуемы, о чем может быть речь?
–1
side2k #
В большинстве штатных ситуаций они, все-таки, предсказуемы.
Другое дело, что гарантий никаких нет 8)
–1
AnatolyB #
«предсказуемость» и «отсутствие гарантий» — это взаимоисключающие вещи.

Может вы путаете «предсказуемость» с «объяснимостью»? По факту, объяснить можно все, но точно предсказать это — очень-очень сложно.
0
side2k #
Это вы несколько путаете — теорию и реальную жизнь.
При бюджете, например, в 50 тысяч никто не будет вам разрабатывать систему реального времени, снимающую показания с измерительных приборов и отражающих их на индикаторах, за которыми наблюдает диспетчер. Есть ГОСТы, регламентирующие время от изменения показателя до отражения этого изменения, и программа(к слову, написанная на Delphi) с этим справляется — за несколько лет никаких проблем. Это я и называю «предсказуемость в штатной ситуации» — если система даже не real-time, то можно увеличить ее предсказуемость, снижая количество влияющих на нее факторов.
Можно сколько угодно пыжиться и надувать щеки в стиле «а вот если вдруг...», «гарантий нет, что ....» — факт в том, что есть решение, устраивающее заказчика(и реально работающее), и по устраивающей разработчика стоимости.
–1
AnatolyB #
> Есть ГОСТы, регламентирующие время от изменения показателя до отражения этого изменения, и программа(к слову, написанная на Delphi) с этим справляется

А каким образом снимаются показания?
0
side2k #
Вообще — по-разному.
Но в общем случае — приходят со счетчика с датой, временем и измерением.
–1
AnatolyB #
Ну каким образом приходит?
С USB-устройства, например, с COM-порта или еще как?
0
side2k #
Да по-разному. Есть такие, которые с COM-порта, есть такие, которые по Ethernet-у бегают(с TCP в качестве транспорта).
–1
AnatolyB #
> Есть ГОСТы, регламентирующие время от изменения показателя до отражения этого изменения

И «отражение измерения» — это что такое?
0
side2k #
1. Его(измерения) появление в БД
2. Его появление на индикаторе, на который смотрит диспетчер
–1
AnatolyB #
Как-то странно все это. Время появления в БД (сферической) — зависит от множества факторов (начиная от прохождения сетевых пакетов, кончая блокировками при выполнении транзакций и т.п.).

А появление сигнала на индикаторе диспетчера, с учетом классического лимита частоты восприятия глазом 25 Гц (0,04 секунды) и попытка его измерения с точностью 0,00001 секунды выглядит тем более странновато.
0
side2k #
1. Ну, БД бывают разные. В данном случае, их несколько, но в данном случае важна лишь одна — ин-мемори, без транзакций. Часть данных являются «высокоприоритетными» — при заполнении их определенными значениями, срабатывают определенные алгоритмы обратной связи(грубо говоря — скакнул ток — сработала защита).
2. Странно, да. Но при сертификации этот фактор не учитывают. Есть отраслевые требования, задокументированные приказом таким-то, хотите получить сертификат — демонстрируйте соответствие.
–1
AnatolyB #
Уточните по пункту 2. Каким образом проверяется время поступления сигнала до его индикации?
0
side2k #
Многовато вопросов задаете, мы уже в сторону ушли 8)
Отвечаю: до индикации время измеряется другой софтиной — эмулятором индикатора, где записываются временные метки значений. Потом достаточно сравнить их с теми, что приходят со счетчика. Ну а время синхронизируется с GPS-приемником.

Неточностей в связи со всем этим — море. Когда я ковырялся с GPS, вскрылись такие бездны «гуляния» системного таймера виндов, что хватались за голову — для обычного процесса там и речи нет даже о точности более чем в 0.1 с. При помощи таких вот статей все эти неточности можно несколько уменьшить. И иногда и это «несколько» может хорошо выручить — будь подобная(только более развернутая и с рассмотрением других вариантов) статья у меня под рукой пару-тройку лет назад — здорово бы сэкономил свое время. Умный, как известно, предпочитает учиться на чужих ошибках.

Real-time систему не на спецжелезе со спецосью не построишь, это факт. Но далеко не всегда требуется настоящий real-time.
–1
AnatolyB #
По-моему, вы там сильно все перемудрили, и с доказательной базой и архитектурой системы не все хорошо. Сложно сказать, что за цифры вы там вообще получали и как они связаны с действительностью.

> Real-time систему не на спецжелезе со спецосью не построишь, это факт. Но далеко не всегда требуется настоящий real-time.

В чем проблема? Запросто!
0
side2k #
Ну, архитектуру мудрил не я. Разработчикам далеко не всегда дают требуемые сроки и возможность делать «как надо бы по-хорошему», вы не знали? 8)
Я еще раз говорю — вы слишком привязаны к теории. В жизни оно по-разному складывается.

>В чем проблема? Запросто!

AVR-ка, мигающая светодиодами? 8))

Проблем разных много, их сложно охватить в рамках дискуссии в комментариях, да и не силен я в матчасти.
–1
AnatolyB #
> AVR-ка, мигающая светодиодами? 8))

Хоть так, хоть DOS под x86. Видел ПО для управление производственной линией под DOS/4GW.
0
side2k #
>Видел ПО

Я сам писал — чувствуете разницу?

>для управление производственной линией под DOS/4GW

DOS тоже отвечает не всем требованям стандартов Real-time ОС. Геморы с драйверами, отсутствие многозадачности, как следствие — отсутствие вменяемого IPC.
–1
AnatolyB #
> Я сам писал — чувствуете разницу?

Нет, не чувствую.

> DOS тоже отвечает не всем требованям стандартов Real-time ОС

И какие же требования для RTOS по-вашему?
0
side2k #
Главное требование для hard real-time — гарантированный результат операции за четко определенный промежуток времени. DOS этого не гарантирует.
0
AnatolyB #
> DOS этого не гарантирует.

Чего??? Под DOS вся аппаратура под контролем, как раз здесь все максимально гарантировано!
0
side2k #
С каких это пор там что-то гарантировано?
А если, например, возникнет прерывание?
Или CLI и побежали? Далеко не убежите 8)
0
AnatolyB #
С самых ранних пор, с самого начала. Потому что есть полный доступ к аппаратуре, максимально возможный.

Есть возможность управлять всеми прерываниями, таймерами, любыми структурами процессора.

И именно на этих возможностях построены другие подобные системы.
0
side2k #
По-моему, вы несколько заблуждаетесь относительно DOS'а.
0
AnatolyB #
Нисколько :)
0
side2k #
Ну на этом тогда и закончим.
С вопросами веры — это к батюшкам в церковь 8)
0
AnatolyB #
Пример реализации простой многозадачной RTOS поверх DOS:

RTKernel — Real-Time Multitasking Kernel for DOS.
0
side2k #
Во-первых, RTKernel по сути практически заменяет DOS(чего разработчики и не отрицают, допуская установку на комп вообще без оси).
Во-вторых, я же вроде изначально говорил о том, что реальная жизнь вносит коррективы. RTKernel, например, сам по себе стоит 1000 евро. А где вы найдете под него разработчика, если речь не про Москву? Сколько такой разработчик запросит денег? А как вы будете поддерживать свой продукт? А если требуется возможность багфикса через интернет?
В-третьих, даже с RTKernel вы получаете soft real-time, а никак не hard real-time. Т.е. от простой программы под Windows оно будет отличаться только тем, что тайминги будут намного(но все же не бесконечно много) надежнее. Гарантии все равно не будет.
0
AnatolyB #
> Во-первых, RTKernel по сути практически заменяет DOS

Это не противоречит ни одной моей фразе, сказанной выше. Наоборот, я об этом много раз повторял.

> RTKernel, например, сам по себе стоит 1000 евро. А где вы найдете под него разработчика

Это уже другой вопрос, не связанный с «можно ли для DOS сделать realtime-приложение»

> В-третьих, даже с RTKernel вы получаете soft real-time, а никак не hard real-time.

С чего вы взяли? Это не так! Как сделаете, так и будет. Все в ваших руках, т.к. доступен самый низкий уровень программирования. Ниже некуда.
0
side2k #
>Это не противоречит ни одной моей фразе, сказанной выше.

Ну, например, он не совсем «поверх» DOS 8)
Да и речь вроде шла на основе DOS'а а не спецсофте — в самом начале ветки я и писал, что спецжелезо и спецсофт требуются для Hard real-time.
Кроме того, я не спец в архитектуре x86, вполне возможно, что и там есть затыки.

>Это уже другой вопрос, не связанный с «можно ли для DOS сделать realtime-приложение»

Не забывайте, что речь началась с вашего вопроса «для чего это нужно». Я объяснил, что soft real-time системы можно строить и на бытовых осях, при определенном подходе.
Ну чего вы реально под DOS навертите(при разумных затратах), если, например, вам обрабатывать надо гигабайты данных в час, где-то их складировать, куда-то их пересылатьи т.п.? Десятком/сотней станков порулить — это одно, а вот тысячей управляемых источников данных?

>Ниже некуда.

А драйвера? А обработчики прерываний?

Дискуссия, по-моему, переходит в затяжную. Если у вас есть аккаунт в Wave-предлагаю обсудить там. Если нет, могу прислать инвайт — пишите почту в личку.
0
AnatolyB #
> если, например, вам обрабатывать надо гигабайты данных в час, где-то их складировать, куда-то их пересылатьи т.п.?

У меня подозрение, что вы не понимаете значения «real time». Оно к производительности не имеет отношения. Система жесткого реального времени — это всего лишь система, в которой ограничено время реакции. Это может быть и миллисекунда, и секунда. Главное в real-time системах — это предсказуемость.

> А драйвера? А обработчики прерываний?

Если вы посмотрите на типичное приложение под DOS, то увидите, что это сборище «драйверов». Для видео, для звука, для принтера, для последовательных портов и т.п. А типичную архитектуру можно представить в виде картинки:



Можете еще в Wiki посмотреть:

Архитектуры операционных систем реального времени
Уровневая (слоевая) архитектура. Пример — MS-DOS. Прикладное ПО имеет возможность получить доступ к аппаратуре не только через ядро системы и ее сервисы, но и напрямую. По сравнению с монолитной такая архитектура обеспечивает значительно большую степень предсказуемости реакций системы, а также позволяет осуществлять быстрый доступ прикладных приложений к аппаратуре.
0
side2k #
P.S. минусы вам проставляю не я, это кто-то набежал со стороны в нашу ветку 8(
0
AnatolyB #
Ну я верю… :)
0
Levsha100 #
Делфи… как давно это было…
Отвык уже от begin-end…
–1
flashnik #
Интересно, насколько он еще используется в реальных проектах?..
+1
JayDi #
Довольно часто. По ссылке показаны примеры популярных программ, написанных на Delphi: www.embarcadero.com/application-showcase
0
nzeemin #
Исчезающе мало.

Смотрим статистику вакансий на Dice.com:
C# — 4800
Java — 10086
Delphi — 53
+1
bolsh #
Методология измерения не совсем правильная. NtDelayExecution переносит поток из списка плнируемых в список ожидания. Повторный запуск произойдет через заданный интервал + время до следующего планирования + время на смену контектов потока. Поэтому маленькие интервалы никогда не будут правильно измеряны в ОС с планировщиками общего назначения. А при больших интервалах кажущаяся точность появляется потому, что отношение необходимой задержки к накладным расходам становится больше — накладные расходы меньше влияют на результат измерения. При переключении задач может также происходить инвалидация кеша процессора, его заполнение требует время, вероятность велика, если код долго не получал управления из-за того, что был отложен — скорее всего неиспользуемый код был вытеснен.

rdtsc также не является реальным показателем, как уже говорили про разные ядра и процессоры также стоит отнести регуляцию частоты в мобильных решениях. Также, с появлением переупорядочивания на конвеере в процессорах Интел не гарантируется, что rdtsc исполнится не раньше, чем предшествующие инструкции.

Таким образом, чтобы точно измерить интервал, надо не выходить в ожидание, а наоборот занять процессор полностью, время от врмени получая метки времени, желательно как-то избегать переключения задач или учесть накладные расходы на переключение — для этого необходимо будет найти способ определения моментов переключения контекстов, Clerk на васме писал про некоторые фичи Windows, которые позволяют засечь момент переключения контекстов.

Также желательно инвалидировать кеши 1 уровня перед замерами и очищать конвеер (например дальним вызовом, только тут может помешать предсказание переходов...).
0
infreerat #
xor eax, eax;
cpuid;
rdtsc;
;
;code under investigation
;
xor eax, eax
cpuid;
rdtsc;
0
bolsh #
Спасибо за хинт :)
+1
shiko_1st #
1. «На некоторых версиях Delphi есть готовая команда rdtsc»
Это не готовая команда на некоторых версиях Delphi, а наличие поддержки встроенным ассемблером оной команды. В Delphi6 уже есть, в старших тем более.

2. Про разные архитектуры процессоров, многоядерность и нереалтаймность Win32 уже сказали.

3. А какое практическое применение у этого метода? Т.е. когда он действительно дает принципиально лучший результат относительно известных стандартных? Ведь если мы имеем дело с получением некоторых отсчетов от некоторого железа, то удобнее всего (и так делается в реальной жизни) использовать метки времени от самого железа.
–2
S_talker #
Недавно нужна была подобная функциональность. За полчасика набросал базовый модуль для профайлинга, основываясь на QueryPerfomanceCounter. Получилась точность порядка 3 микросекунд (к сожалению, не помню, какой был процессор). Если бы использовать ассемблерные вставки, и дополнительные оптимизации, то, думаю, можно было бы догнать до 1 мкс (а может и еще точнее); но на тот момент мне данной точности было достаточно.
–1
andreyu #
Решение, которое работает у меня:

#include

struct timeval start, end;
gettimeofday(&start, NULL);

// your code here

gettimeofday(&end, NULL);
long seconds = end.tv_sec — start.tv_sec;
long useconds = end.tv_usec — start.tv_usec;
long mtime = ((seconds) * 1000 + useconds/1000.0) + 0.5;
printf(«Elapsed time: %ld milliseconds\n», mtime);
–1
cronoc #
хороша статья) в мемориз)
0
cronoc #
жаль голоса на сегодня закончились) автор, в случае чего — стучи, плюсануть карму и топик однозначно надо)

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