DevDoc home page DevDoc background gradient
 
   
Имя Пароль Запомнить Зарегистрироваться

Автор: Кудинов Александр
Последняя модификация: 2010-10-25 01:56:29

QueryPerformanceCounter - бомба замедленного действия

Все примеры написаны с использованием библиотеки QT

Введение

Однажды мне понадобилось сделать программу, которая работает по собственным часам. Основное требование - часы должны быть нечувствительными к изменению системного времени. Казалось бы, нет ничего проще, однако...

Системные функции, которые непосредственно используются для получения времени/даты привязаны к часам Windows. Сообщение WM_TIMER не гарантирует точности, ядро может пропускать или объединять сообщения, если процессор перегружен.

Тут я вспомнил про чудесные функции QueryPerformanceCounter & QueryPerformanceFrequency. Эта пара считает временные интервалы по тактам процессора.

Требования к моей программе допускали небольшую неточность - часы могли отставать на пару секунд в сутки. А при частоте процессора в пару ГГц не было и речи о том, что часы будут идти неверно. В общем, эта пара API функций с лихвой перекрывала мои потребности. Кроме того, они сами по себе работают очень быстро, и, как утверждает Microsoft - они работают даже на многопроцессорных/многоядерных системах без ошибок. Желающие могут сами посчитать погрешность измерений.

Реализация

Реализация часов очень простая. При старте программы мы получаем "базовое время". В самом простом случае можно просто взять текущее время системы. Я загружал точное время с сервера через сеть. Одновременно с этим мы запоминаем значение счетчика тактов, который получаем вызовом QueryPerformanceCounter.

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

QDateTime m_tmZerroTime = QDateTime::currentDateTime();
	LARGE_INTEGER li;
	QueryPerformanceCounter(&li);
	m_tmQP = li.QuadPart;
 
	:.
	QDateTime tmCurrent = QDateTime::currentDateTime();
	LARGE_INTEGER _TC, _TF;
	QueryPerformanceCounter(&_TC);
	QueryPerformanceFrequency(&_TF);
	QDateTime tmQC = m_tmZerroTime.addMSecs( 1000 * (_TC.QuadPart - m_tmQP) / _TF.QuadPart );
	//tmQC Содержит значение наших часов

Очень просто, не правда ли? Значит продолжим.

Эти часы исправно проработали больше 3-х месяцев в готовой программе, а потом начались необъяснимые глюки. Некоторые пользователи стали жаловаться, что часы в программе отстают от часов в системе. Программа содержала огромное количество бизнес логики и поиски ошибки затянулись. Потом случайно заметили, что наши часы начинают врать на ноутбуках, когда их переводят на питание от батарейки. Тогда закралось подозрение на QueryPerformanceХХХ, хотя горячо любимый Майкрософт в МСДН утверждает, что частота тиков не меняется после старта системы.

Тогда я сделал простое тестовое приложение, которое замеряло разницу между системными часами и моей реализацией. Тестовое приложение проще некуда и там просто нет места ошибкам. Каково же было мое удивление, когда выяснилось, что на моем ноутбуке QueryPerformanceXXXX стало показывать дрейф тактов процессора.

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

Ребята из MS не отрицают существование этой проблемы, хотя об этом нет не единого слова в документации на QueryPerformanceCounter & QueryPerformanceFrequency. По их мнению, счетчик тиков съезжает скачками (что видно в тестовом приложении) из-за багов (!?) в некоторых чипсетах. Мое мнение, что счетчик работает неверно, потому что современные процессоры могут менять свою частоту на лету, чтобы экономить энергию. Особенно это актуально для ноутбуков.

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

В моем случае я решил проблему очень просто. Я заменил QueryPerformanceXXXX на GetTickCount(). Последняя функция обеспечивает достаточную точность для моих целей, не подвержена скачкам и не привязана к системным часам. Получилось даже проще, чем было:

QDateTime m_tmZerroTime = QDateTime::currentDateTime();
	DWORD m_tmTC = GetTickCount();
 
	...
 
	//tmTC содержит значение наших часов.
	QDateTime tmTC = m_tmZerroTime.addMSecs( GetTickCount() - m_tmTC );

PS/ Вместо GetTickCount можно попробовать использовать функцию timeGetTime() - она более точная и на первый взгляд более стабильная.

Подпишитесь, чтобы получать все новые статьи с сайта первым!

Оценить статью Текущий рейтинг: 4.2857. Проголосовало 7 человек.

Коментарии к статье

Нет комментариев

Добавить комментарий

Вы можете добавлять свои комментарии, пожелания или вопросы к статье. Они будут видны всем пользователям, а также автору статьи. Войдите в систему под своим именем, чтобы другие пользователи могли видеть, кто автор сообщения. Форма для входа расположена вверху страницы. Если у Вас нет аккаунта на сайте - рекомендуем зарегистрироваться. Это займет всего пару минут.

Отправлять сообщения могут только зарегистрированные пользователи

Copyright (C) Kudinov Alexander, 2006-2012

Перепечатка материалов с данного сайта запрещена без письменного разрешения автора. При перепечатке обязательно указывать ссылку на оригинал.

Generation time: 0,157655000687 seconds