QueryPerformanceCounter и переполнения - PullRequest
5 голосов
/ 14 марта 2011

Я использую QueryPerformanceCounter, чтобы определить время в моем приложении.Однако после нескольких дней работы приложение перестает работать правильно.Если я просто перезапускаю приложение, оно снова начинает работать.Это заставляет меня поверить, что у меня проблема переполнения в моем временном коде.

// Author: Ryan M. Geiss
// http://www.geisswerks.com/ryan/FAQS/timing.html
class timer
{
public:
    timer()
    {
        QueryPerformanceFrequency(&freq_);
        QueryPerformanceCounter(&time_);
    }

    void tick(double interval)
    {       
        LARGE_INTEGER t;
        QueryPerformanceCounter(&t);

        if (time_.QuadPart != 0)
        {
            int ticks_to_wait = static_cast<int>(static_cast<double>(freq_.QuadPart) * interval);
            int done = 0;
            do
            {
                QueryPerformanceCounter(&t);

                int ticks_passed = static_cast<int>(static_cast<__int64>(t.QuadPart) - static_cast<__int64>(time_.QuadPart));
                int ticks_left = ticks_to_wait - ticks_passed;

                if (t.QuadPart < time_.QuadPart)    // time wrap
                    done = 1;
                if (ticks_passed >= ticks_to_wait)
                    done = 1;

                if (!done)
                {
                    // if > 0.002s left, do Sleep(1), which will actually sleep some 
                    //   steady amount, probably 1-2 ms,
                    //   and do so in a nice way (cpu meter drops; laptop battery spared).
                    // otherwise, do a few Sleep(0)'s, which just give up the timeslice,
                    //   but don't really save cpu or battery, but do pass a tiny
                    //   amount of time.
                    if (ticks_left > static_cast<int>((freq_.QuadPart*2)/1000))
                        Sleep(1);
                    else                        
                        for (int i = 0; i < 10; ++i) 
                            Sleep(0);  // causes thread to give up its timeslice
                }
            }
            while (!done);            
        }

        time_ = t;
    }       
private:    
    LARGE_INTEGER freq_;
    LARGE_INTEGER time_;
};

Мой вопрос заключается в том, должен ли приведенный выше код работать детерминистически в течение нескольких недель непрерывной работы?

А если нет, то где проблема?Я думал, что переполнение было обработано

if (t.QuadPart < time_.QuadPart)    // time wrap
    done = 1;

Но, может быть, этого недостаточно?

РЕДАКТИРОВАТЬ: Пожалуйста, обратите внимание, что я не написал оригинальный код, Райан М. Гейсс, ссылка наисходный код кода находится в коде.

Ответы [ 5 ]

15 голосов
/ 14 марта 2011

QueryPerformanceCounter известен своей ненадежностью.Это хорошо для индивидуальной синхронизации на короткие промежутки времени, если вы готовы к ненормальным результатам.Это , а не точно - обычно оно основано на частоте шины PCI, а сильно загруженная шина может привести к потере тиков.

GetTickCount на самом деле более стабильна и может дать вам 1 мсразрешение, если вы позвонили timeBeginPeriod.Это в конечном итоге будет перенесено, так что вам нужно с этим справиться.

__rdtsc не следует использовать, если вы не профилируете и не контролируете, какое ядро ​​вы используете и готовы обрабатывать переменную частоту ЦП..

GetSystemTime подходит для более длительных периодов измерений, но будет изменяться при настройке системного времени.

Кроме того, Sleep(0) делает не то, что вы думаете, что делает.Он выдаст процессор , если этого захочет другой контекст - в противном случае он сразу же вернется.

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

Лучше всего было бы сделать ставкупросто используйте GetTickCount или GetSystemTime, оберните его во что-то, что настраивает для скачков времени / обходов.

Кроме того, вы должны преобразовать double interval в int64 milliseconds и затем использовать только целочисленную математику - это позволяет избежать проблем из-зас переменной точностью типов с плавающей точкой в ​​зависимости от их содержания.

5 голосов
/ 14 марта 2011

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

См. Следующие примеры:

5 голосов
/ 14 марта 2011

Счетчики производительности являются 64-разрядными, поэтому они достаточно велики для многолетней непрерывной работы. Например, если предположить, что счетчик производительности увеличивается в 2 миллиарда раз в секунду (некоторый воображаемый процессор с частотой 2 ГГц), он будет переполнен примерно через 290 лет.

4 голосов
/ 14 марта 2011

Использование таймера наносекундной шкалы для управления чем-то вроде Sleep (), для которого в лучшем случае с точностью до нескольких миллисекунд (и обычно нескольких десятков миллисекунд) в любом случае несколько противоречиво.

Другой подход, который вы могли бы рассмотреть, заключается в использовании WaitForSingleObject или аналогичной функции. Это сжигает меньше циклов ЦП, вызывает переключение контекста на триллион в день и более надежно, чем Sleep (0).

Вы можете, например, создать семафор и никогда не трогать его при нормальной работе. Семафор существует только для того, чтобы вы могли ждать что-то , если вам нечего ждать. Затем вы можете указать время ожидания в миллисекундах до 49 дней с помощью одного системного вызова. И это будет не только меньше работать, но и будет намного точнее.

Преимущество состоит в том, что если «что-то происходит», поэтому вы хотите разойтись раньше, вам нужно только дать сигнал семафору. Ожидание вызова немедленно вернется, и из возвращаемого значения WAIT_OBJECT_0 вы узнаете, что оно было вызвано сигнализацией, а не истечением времени. И все это без сложной логики и циклов счета.

2 голосов
/ 15 апреля 2014

Проблема, о которой вы спрашивали больше всего: if (t.QuadPart < time_.QuadPart) должно быть следующим: if (t.QuadPart - time_.QuadPart < 0)

Причина этого заключается в том, что вы хотите искать перенос в относительном, а не абсолютном времени.Относительное время будет перенесено (1 раз << 63) единиц времени после обращения к QPC.Абсолютное время <em>может переносить (1 раз << 63) единицы времени после перезагрузки, но оно может переноситься в любое другое время, когда это ощущается, это не определено.</p>

QPC немного ошибочен в некоторых системах (например, в старых QPC на основе RDTSC в ранних многоядерных процессорах), поэтому может быть желательно разрешить небольшие отрицательные дельты времени, например: if (t.QuadPart - time_.QuadPart < -1000000) // time wrap

Фактическая упаковка даст очень большие отрицательные дельты времени, так что это безопасно.Это не должно быть необходимо в современных системах, но доверие к Microsoft редко является хорошей идеей.

... Тем не менее, большая проблема с временным переносом заключается в том, что ticks_to_wait, ticks_passed и ticks_left имеют тип int, а не LARGE_INT или long longкак они должны быть.Это делает большую часть этого кода переносом, если задействованы какие-либо значимые периоды времени - и «значительный» в этом контексте зависит от платформы, он может быть порядка 1 секунды в некоторых (редких в наши дни) случаях или даже меньше в некоторыхгипотетическая система будущего.

Другие проблемы:

if (time_.QuadPart != 0)

Ноль не является особым значением и не должен рассматриваться как таковой.Я предполагаю, что код совмещает QPC, возвращая нулевое время с возвращаемым значением QPC, равным нулю.Возвращаемое значение - это не 64-битное время, переданное указателем, это BOOL, который фактически возвращает QPC.

Кроме того, этот цикл Sleep (0) является глупым - он настроен на корректную работу только на определенном уровне конкуренции и конкретной производительности ЦП для каждого потока.Если вам нужно разрешение, это ужасная идея, и если вам не нужно разрешение, тогда вся эта функция должна была быть всего лишь одним вызовом Sleep.

...