Nelek Ответов: 2

Получение максимально точной временной метки


Сценарий:
Я разрабатываю новый модуль для генерации файлов протоколов в специальном формате, который будет интегрирован в более крупное программное обеспечение. Основное программное обеспечение состоит из нескольких частей, некоторые из которых написаны на языке C# .Net и некоторые из них на c++ с MFC (включая многопоточность), а API, который я использую, тоже имеет файл .c.
Мое тестовое приложение-это консоль C++ с поддержкой MFC и ATL в VS2017 Professional.
Хотя основное программное обеспечение использует многопоточность, я не хочу связываться с ними в своем тестовом приложении.

Я хотел бы получить самую быструю (с точки зрения производительности) и самую точную / наименьшую временную метку разрешения для вычисления дельт / необходимого времени выполнения, которое я могу использовать.
Возможность использования дельт в другой части программы еще не исчезла, поэтому я хотел бы получить ее в переменной на всякий случай. Вот почему я стараюсь делать измерения с помощью кода, а не с помощью внешних инструментов производительности.

Что я уже пробовал:

Я попробовал несколько подходов:

1)
time_t GetActualTime()
{
  _tzset();

  time_t myTime;
  time(&myTime);

  return myTime;
}
Я полагаю, что это самый быстрый вариант (во время выполнения), но он возвращает секунды. Для других частей моей программы этого более чем достаточно, но не для того, что я хочу проверить прямо сейчас.

2)
С FILETIME я должен быть в состоянии получить до 100x наносекунд, но проблема заключается в "обходном пути", который я использую, чтобы получить необходимую метку времени. С этим кодом:
ULARGE_INTEGER GetTimeFile100xNanoSec (int iNr)
{
  FILETIME timeCreation;        // Value in 100x NanoSecons
  ULARGE_INTEGER uli100xNanoSec;
  
  CString strFileName = _T("");
  strFileName.Format(_T("D:\\Temp\\myDummyFile_%03d.txt"), iNr); // to avoid tunnelig effect if using same name
  CStringW strFileNameW(strFileName);
  LPCWSTR lpFileName = strFileNameW;

  HANDLE hFile = CreateFileW(lpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

  GetFileTime(hFile, &timeCreation, NULL, NULL);
  CloseHandle(hFile);
  DeleteFileW(lpFileName);

  uli100xNanoSec.LowPart = timeCreation.dwLowDateTime;
  uli100xNanoSec.HighPart = timeCreation.dwHighDateTime;
 
  return uli100xNanoSec;
}
используя его в цикле, как:
ULARGE_INTEGER lluOld;
ULARGE_INTEGER lluNew;

lluOld = GetTimeFile100xNanoSec(999);
for (int i=0; i<100; i++)
{
  lluNew = GetTimeFile100xNanoSec(i);
  wprintf(_T("\nIn Loop [%d], New - old = %llu"), i, lluNew.QuadPart - lluOld.QuadPart);
  lluOld.QuadPart = lluNew.QuadPart;
}

Я получаю значения от 10001 (1 мс) до 60006 (6 мс), среднее значение 100 попыток составляет почти 2,5 МС. Создание / удаление временных файлов влияет на производительность, делая ее настолько медленной, что допустимый диапазон достигает миллисекунд. Что заставляет маленькое решение быть напрасным.


3)
С системным временем я могу спуститься только до миллисекунд. Я еще не проверил скорость работы, но я сделаю это позже, если я смогу получить Шаг 1 мс стабильным способом, я полагаю, что буду использовать это, так как #2 недостаточно стабилен, чтобы быть надежным


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

2 Ответов

Рейтинг:
16

Rick York

Я использую небольшой класс-оболочку вокруг функции API QueryPerformanceCounter. Вот его урезанная версия:

class CElapsed
{
public :
   CElapsed()   // constructor
   {
        // get the frequency of the performance counter and its period in seconds

        LARGE_INTEGER li = { 0 };
        m_Period = QueryPerformanceFrequency( &li ) ? 1.0 / (double)li.QuadPart : 0;
   }

   // get the current performance counter value, convert it
   // to seconds, and return the difference from begin in seconds

   double TimeSince( double begin=0 )
   {
       LARGE_INTEGER endtime;
       QueryPerformanceCounter( &endtime );
       return ( endtime.QuadPart * m_Period ) - begin;
   }

   // returns true if the counter is available

   bool IsAvailable()     { return m_Period != 0; }

   // return the counter frequency

   double GetFrequency()  { return 1.0 / m_Period; }

protected :
   double  m_Period;
};
Вот пример того, как его использовать :
CElapsed et;
double start = et.TimeSince( 0 );

// code to time goes here

double elapsed = et.TimeSince( start );
_tprintf( _T( "elapsed time was %.3f seconds\n" ), elapsed );
Я рекомендую прочитать о QueryPerformanceCounter, чтобы узнать, каковы его свойства. Частота счетчика зависит от машины, но почти всегда находится в диапазоне мегагерц, поэтому его разрешение находится в микросекундах, но у него есть некоторые накладные расходы. Мне никогда не требовалось время с разрешением менее миллисекунды, так что это вполне подходит для моих целей.

Существует множество вариаций этого типа класса таймеров. Мне нравится эта реализация, потому что она не сохраняет начальное значение. Это позволяет использовать один объект таймера для многих вещей одновременно. Фактически, один объект таймера можно использовать для всего приложения, если вы хотите это сделать. Конструкция также минимальна, поэтому экземпляры могут быть созданы быстро и легко с минимальными накладными расходами.


Nelek

Я не читал об этом, потому что он находится на том же сайте, что и SetTimer и WaitableTimerObject. Я не видел удобства использования этих двух, поэтому просто проигнорировал остальные (неправильно... Я знаю)

Выглядеть красиво. Я посмотрю и сделаю несколько тестов. Спасибо

[no name]

Также: https://www.codeproject.com/Questions/480201/whatplusQueryPerformanceFrequencyplusfor-3f

Rick York

Да, только терминология у него обратная. QueryPerformanceFrequency дает частоту счетчика. Один над частотой-это период счетчика в секундах. Одна тысяча по частоте-это период в миллисекундах.

Nelek

Привет Рик,

Я проверил вашу обертку, и она мне очень нравится. Меня немного раздражает, что он все еще так ненадежен в исполнении :( я имею в виду:
Я проверил это так:

CElapsed et;
double start = et.TimeSince(0.0);
double absolute = 0.0;
double delta = 0.0;
double old = 0.0;
wprintf(_T("\nPrior loop: start TimeSince (0.0) = %lf\n"), start);
for (int j = 0; j < 100; j++)
{
absolute = et.TimeSince(start);
delta = absolute - old;
wprintf(_T("\nIn Elapsed Delta ABSOLUTE [%d] = %lf"), j, absolute);
wprintf(_T("\nIn Elapsed Delta RELATIVE [%d] = %lf"), j, delta);
wprintf(_T("\n----------"));
old = absolute;
}

и он все еще колеблется от 0,000271 (271 микросекунда) самого быстрого тика во всех моих тестах (10 исполнений на 100 итераций) до самого медленного 0,006823 (6,8 МС).
Обычно он быстр в начале цикла (около 0,7 МС), становится медленнее после некоторых итераций (от 1 до 4 мс) и остается таковым некоторое время, снова становясь быстрым в конце (от 0,5 до 0,8 МС)

Я не знаю, правильно ли я его использую, но я ожидал бы, что он будет немного более постоянным с дельтами

Nelek

Я сократил цикл до 10 итераций. После 20 исполнений я получил 95% значений ниже 0,5 мс (самое медленное-0,8 МС).

Это должно покрыть мои потребности.

Большое вам спасибо за информацию

Rick York

Добро пожаловать. Я рад, что это было полезно.

Рейтинг:
0

OriginalGriff

Видеть здесь: Функция GetTickCount | Microsoft Docs[^] и здесь: О таймерах - приложения для Windows | Microsoft Docs[^]

Разрешение не фиксировано: оно варьируется в зависимости от системы, поэтому то, что вы получаете на двух разных компьютерах, работающих под управлением одного и того же программного обеспечения, может быть разным.


Nelek

Не уверен, что ошибка была на мне, но я уже сделал несколько тестов с GetTickCount, и я бы сказал, что это было так нет возвращение [цитата] Возвращаемое значение-это количество миллисекунд, прошедших с момента запуска системы [/quote]. Я буду посмотреть еще раз.
Насчет таймеров... Честно говоря, я отклонил эту страницу, потому что операции таймера и WaitableTimerObject не вписываются в то, что я хочу сделать. Но, увидев решение № 2, я должен был прочитать всю страницу немного более внимательно.