QElapsedTimerの落とし穴と対策: msecsSinceReference()使用時のよくあるエラーとトラブルシューティング

2025-05-27

QElapsedTimerクラスは、時間の測定、特にプログラムの実行時間やある処理にかかった時間を正確に測定するためにQtで提供されるクラスです。

msecsSinceReference()メソッドは、このQElapsedTimerオブジェクトが「参照点」から経過したミリ秒数をqint64型(64ビット整数)で返します。

「参照点」とは?

QElapsedTimerオブジェクトの「参照点」は、以下のいずれかのタイミングになります。

  1. オブジェクトの作成時
    QElapsedTimerオブジェクトが作成(インスタンス化)された時点。
  2. start()メソッド呼び出し時
    QElapsedTimerオブジェクトのstart()メソッドが明示的に呼び出された時点。
  3. restart()メソッド呼び出し時
    QElapsedTimerオブジェクトのrestart()メソッドが呼び出された時点。このメソッドは、タイマーをリセットし、同時に新しい参照点を設定します。

使用例

一般的な使用シナリオとしては、ある処理の開始時にQElapsedTimerをスタートさせ、処理終了時にmsecsSinceReference()を呼び出して経過時間を取得します。

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QDebug>
#include <QThread> // 例としてQThread::msleep()を使用

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QElapsedTimer timer; // QElapsedTimerオブジェクトを作成

    // --- 処理1の時間を測定 ---
    timer.start(); // タイマーをスタート(これが参照点となる)
    QThread::msleep(100); // 100ミリ秒待機する(例)
    qint64 elapsed1 = timer.msecsSinceReference(); // 参照点からの経過時間を取得
    qDebug() << "処理1にかかった時間:" << elapsed1 << "ms";

    // --- 処理2の時間を測定(タイマーをリスタートして再測定) ---
    timer.restart(); // タイマーをリセットし、新しい参照点を設定
    QThread::msleep(250); // 250ミリ秒待機する(例)
    qint64 elapsed2 = timer.msecsSinceReference(); // 新しい参照点からの経過時間を取得
    qDebug() << "処理2にかかった時間:" << elapsed2 << "ms";

    return a.exec();
}

上記のコードを実行すると、以下のような出力が得られます(環境によって若干の誤差はあります)。

処理1にかかった時間: 100 ms
処理2にかかった時間: 250 ms

特徴

  • スレッドセーフ
    基本的にスレッドセーフに設計されていますが、複数のスレッドから同じQElapsedTimerオブジェクトを同時に操作する場合は注意が必要です。
  • 高精度
    非常に高い精度で時間を計測できます。内部的には、利用可能な最も高精度なシステムクロックを利用します。
  • 単調増加
    QElapsedTimerは「単調増加するクロック(monotonic clock)」を使用します。これは、システムの時刻設定が変更されても(例えば、ユーザーが手動でPCの時間を進めたり戻したりしても)、タイマーの経過時間は常に正しく計測されることを意味します。システム時刻に依存するQDateTime::currentMSecsSinceEpoch()のような関数とは異なります。


QElapsedTimer自体は非常に堅牢なクラスであり、msecsSinceReference()メソッドもシンプルに経過時間を返すだけなので、直接的な「エラー」が発生することは稀です。しかし、誤った使い方や期待と異なる結果になることがあり、これらが「トラブルシューティング」の対象となります。

エラー/問題: 経過時間が常に0または非常に小さい値になる

原因

  • 測定対象の処理が非常に高速で、ミリ秒以下の精度でしか時間がかからない場合。
  • start()またはrestart()が呼び出されていない、あるいはmsecsSinceReference()の直前に呼び出されているため、タイマーの参照点が正しく設定されていない。

トラブルシューティング

  • 非常に高速な処理の確認
    もし処理がミリ秒単位で計測できないほど高速である場合、msecsSinceReference()は0を返すか、システムの最小ティック単位の値(例えば1ms)を返すことがあります。これはエラーではなく、処理が高速すぎるためです。より高精度な計測が必要な場合は、Qtの範囲外のOSネイティブAPI(例: WindowsのQueryPerformanceCounter)を検討する必要があるかもしれませんが、通常はQElapsedTimerで十分です。
  • start()またはrestart()の呼び出し忘れ/位置の確認
    タイマーで時間を測定したい処理の直前timer.start()またはtimer.restart()を呼び出しているか確認してください。
    QElapsedTimer timer;
    // ... 他の処理 ...
    timer.start(); // ここでタイマーをスタート!
    // 測定したい処理
    qint64 elapsed = timer.msecsSinceReference();
    

エラー/問題: 予想よりも大きな経過時間が返される

原因

  • デバッグツール(ブレークポイントなど)を使用している場合、ブレークポイントで止まっている間もタイマーは進み続けるため、見かけ上の時間が長くなる。
  • start()またはrestart()を呼び出してからmsecsSinceReference()を呼び出すまでの間に、測定対象ではない不要な処理が含まれている。

トラブルシューティング

  • デバッグ中の測定
    デバッグ中にQElapsedTimerで時間を測定する場合、ブレークポイントでプログラムが停止している時間もカウントされます。これは正常な動作ですが、実際の処理時間を誤解する原因になります。正確なパフォーマンス測定は、リリースビルドでデバッグなしで実行すべきです。
  • 測定範囲の明確化
    start()msecsSinceReference()の間に、本当に測定したい処理だけがあることを確認してください。
    QElapsedTimer timer;
    // タイマー開始前に必要な準備(測定対象外)
    setupData();
    
    timer.start(); // ここから測定開始!
    performComplexCalculation(); // 測定したい処理
    qint64 elapsed = timer.msecsSinceReference(); // ここで測定終了!
    
    // タイマー終了後の後処理(測定対象外)
    cleanupData();
    

エラー/問題: 複数回測定するときに値がおかしくなる

原因

  • restart()を使用せずにstart()を複数回呼び出そうとしている。QElapsedTimer::start()は、すでに起動しているタイマーを再起動することはありません。新しい参照点を設定するにはrestart()を使用する必要があります。

トラブルシューティング

  • restart()の使用
    同じQElapsedTimerオブジェクトで複数の処理の時間を測定する場合や、同じ処理を繰り返し測定する場合は、各測定の開始時にrestart()を使用してください。
    QElapsedTimer timer;
    
    // 1回目の測定
    timer.start(); // または timer.restart();
    doSomething();
    qDebug() << "Do something:" << timer.msecsSinceReference() << "ms";
    
    // 2回目の測定(timerをリセットして新しい参照点を設定)
    timer.restart();
    doAnotherThing();
    qDebug() << "Do another thing:" << timer.msecsSinceReference() << "ms";
    

エラー/問題: 別のスレッドでQElapsedTimerを使うと挙動がおかしい

原因

  • QElapsedTimer自体はスレッドセーフに設計されていますが、同じQElapsedTimerインスタンスを複数のスレッドから同時にstart()restart()msecsSinceReference()などで操作した場合、競合状態が発生する可能性があります。

トラブルシューティング

  • 共有インスタンスの同期
    もしどうしても同じQElapsedTimerインスタンスを複数のスレッドで共有する必要がある場合は、QMutexなどを用いてアクセスを同期する必要があります。しかし、これは複雑になりがちで、パフォーマンスのオーバーヘッドも発生するため、ほとんどの場合推奨されません。
  • スレッドごとのインスタンス
    各スレッドで独自のQElapsedTimerインスタンスを作成し、それを使用するのが最も安全で推奨される方法です。
    // スレッドA
    void MyThreadA::run() {
        QElapsedTimer timerA;
        timerA.start();
        // ... スレッドAの処理 ...
        qDebug() << "Thread A elapsed:" << timerA.msecsSinceReference() << "ms";
    }
    
    // スレッドB
    void MyThreadB::run() {
        QElapsedTimer timerB;
        timerB.start();
        // ... スレッドBの処理 ...
        qDebug() << "Thread B elapsed:" << timerB.msecsSinceReference() << "ms";
    }
    

エラー/問題: タイマーがずっと動いているのに、返される値が小さい/同じ値のまま

原因

  • これは通常、QElapsedTimerの問題ではなく、プログラムのロジックの問題です。タイマーが「止まっている」と誤解しているか、msecsSinceReference()を呼び出している頻度が低いか、ループが非常に短い期間で終わっている可能性があります。
  • プログラムのフローの確認
    msecsSinceReference()が呼び出されるタイミングと、それまでの間にstart()が呼び出されたタイミングを確認してください。例えば、非常に短いループ内でmsecsSinceReference()を呼び出している場合、ほとんど時間が経過していないため同じ値が連続して返されることがあります。


単一の処理時間の測定

最も基本的な使用例です。ある関数やコードブロックの実行にかかった時間を測定します。

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QDebug>
#include <QThread> // 例としてQThread::msleep()を使用

// 測定したい処理の例
void simulateSlowOperation() {
    // 実際に何らかの重い処理をシミュレート
    QThread::msleep(200); // 200ミリ秒待機
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    QElapsedTimer timer; // QElapsedTimer オブジェクトを作成

    qDebug() << "--- 処理時間の測定開始 ---";

    timer.start(); // タイマーを開始(これが参照点となる)

    simulateSlowOperation(); // 測定したい処理を実行

    qint64 elapsedMs = timer.msecsSinceReference(); // 参照点からの経過時間をミリ秒で取得

    qDebug() << "処理にかかった時間: " << elapsedMs << " ms";
    qDebug() << "--- 処理時間の測定終了 ---";

    return a.exec();
}

解説

  • qint64 elapsedMs = timer.msecsSinceReference(); で、start()を呼び出してから現在までの経過時間をミリ秒単位で取得します。この値がelapsedMsに格納されます。
  • simulateSlowOperation(); が実際に時間を計測したいコードです。
  • timer.start(); でタイマーを開始します。これ以降の時間が計測の対象となります。
  • QElapsedTimer timer; でタイマーオブジェクトを宣言します。

複数の処理の時間を順次測定する

同じQElapsedTimerオブジェクトを使って、複数の異なる処理の時間を順番に測定する場合です。この場合、各測定の開始時にrestart()を使用するのが一般的です。

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QDebug>
#include <QThread>

void simulateTaskA() { QThread::msleep(150); }
void simulateTaskB() { QThread::msleep(300); }
void simulateTaskC() { QThread::msleep(50);  }

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    QElapsedTimer timer;

    qDebug() << "--- 複数処理の測定開始 ---";

    // タスクAの測定
    timer.start(); // 最初の測定なので start()
    simulateTaskA();
    qDebug() << "タスクAにかかった時間: " << timer.msecsSinceReference() << " ms";

    // タスクBの測定 (restart()でタイマーをリセットし、新しい参照点を設定)
    timer.restart();
    simulateTaskB();
    qDebug() << "タスクBにかかった時間: " << timer.msecsSinceReference() << " ms";

    // タスクCの測定 (再度 restart())
    timer.restart();
    simulateTaskC();
    qDebug() << "タスクCにかかった時間: " << timer.msecsSinceReference() << " ms";

    qDebug() << "--- 複数処理の測定終了 ---";

    return a.exec();
}

解説

  • タスクBとタスクCの測定にはrestart()を使用します。restart()は、タイマーを停止してゼロに戻し、すぐに新しい参照点からカウントを開始します。これにより、前の処理の時間に影響されずに、その時点からの経過時間を正確に測定できます。
  • 最初のタスクAの測定にはstart()を使用します。

ループ処理における時間制限

hasExpired()メソッドと組み合わせることで、特定の時間内に処理を完了させる、あるいは指定時間だけループを実行するといった用途にも使えます。msecsSinceReference()自体が直接使われるわけではありませんが、時間経過の概念が重要になります。

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QDebug>
#include <QThread>

// 何らかの小さい処理
void doWorkUnit() {
    // 実際には計算やデータ処理などを行う
    // 例として非常に短い待機を挟む
    QThread::msleep(1);
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    QElapsedTimer timer;
    const qint64 totalTimeoutMs = 1000; // 合計1000ミリ秒(1秒)で処理を試みる
    int workCount = 0;

    qDebug() << "--- 時間制限付き処理の開始 ---";

    timer.start(); // タイマーを開始

    // タイマーが指定されたタイムアウト時間まで経過していない間、処理を続ける
    while (!timer.hasExpired(totalTimeoutMs)) {
        doWorkUnit();
        workCount++;
        // 必要に応じて途中経過を表示
        if (workCount % 100 == 0) {
            qDebug() << "経過時間: " << timer.msecsSinceReference() << "ms, 処理回数: " << workCount;
        }
    }

    qDebug() << "--- 時間制限付き処理の終了 ---";
    qDebug() << "最終経過時間: " << timer.msecsSinceReference() << " ms";
    qDebug() << "総処理回数: " << workCount;

    return a.exec();
}

解説

  • qDebug() << "経過時間: " << timer.msecsSinceReference() << "ms, ..."; のように、ループの途中で現在の経過時間を取得し、進行状況を確認することもできます。
  • ループ内でdoWorkUnit()という小さな処理を繰り返し実行し、workCountをカウントアップします。
  • while (!timer.hasExpired(totalTimeoutMs)) ループは、totalTimeoutMsで指定された時間が経過するまで継続します。
  • timer.start(); で時間測定を開始します。

msecsTo()メソッドを使うことで、2つの異なるQElapsedTimerインスタンス間の時間差を測定することもできます。msecsSinceReference()とは少し異なりますが、時間測定の応用例として役立ちます。

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QDebug>
#include <QThread>

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    QElapsedTimer timer1;
    QElapsedTimer timer2;

    qDebug() << "--- 複数タイマー間の時間差測定開始 ---";

    timer1.start(); // 最初のタイマーを開始

    QThread::msleep(100); // 100ms 待機

    timer2.start(); // 2番目のタイマーを開始

    QThread::msleep(50); // さらに 50ms 待機

    qint64 elapsedSinceTimer1Start = timer1.msecsSinceReference();
    qint64 elapsedSinceTimer2Start = timer2.msecsSinceReference();
    qint64 diff = timer1.msecsTo(timer2); // timer1からtimer2までの時間差

    qDebug() << "timer1開始からの経過時間: " << elapsedSinceTimer1Start << " ms";
    qDebug() << "timer2開始からの経過時間: " << elapsedSinceTimer2Start << " ms";
    qDebug() << "timer1からtimer2までの時間差 (msecsTo): " << diff << " ms";

    qDebug() << "--- 複数タイマー間の時間差測定終了 ---";

    return a.exec();
}
  • timer1.msecsTo(timer2) は、timer1の参照点からtimer2の参照点(timer2.start()が呼び出された時点)までのミリ秒数を返します。
  • timer1.start() の後、少し待ってから timer2.start() を呼び出します。
  • timer1timer2 の2つの独立したタイマーを使用します。


QTime クラス

QTimeはQtが提供する古い時間クラスで、時刻や時間間隔を扱うことができます。QElapsedTimerが登場する以前は、処理時間の測定にもよく使われていました。

特徴

  • Qt 4.7より前のバージョンで主に使用
    QElapsedTimerがQt 4.7以降で導入されたため、古いQtバージョンで時間測定を行う場合はQTimeが選択肢となります。
  • ミリ秒精度
    基本的にミリ秒精度での計測が可能です。
  • システム時刻に依存
    QTimeはシステムのウォールクロック(壁時計)に依存します。そのため、システムの時刻設定が変更されると、測定結果も影響を受ける可能性があります。これはQElapsedTimerの単調増加クロックとは異なります。

使用例

#include <QCoreApplication>
#include <QTime>
#include <QDebug>
#include <QThread>

void simulateSlowOperation() {
    QThread::msleep(200); // 200ミリ秒待機
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    QTime timer;

    qDebug() << "--- QTimeでの処理時間測定開始 ---";

    timer.start(); // タイマーを開始

    simulateSlowOperation(); // 測定したい処理を実行

    int elapsedMs = timer.elapsed(); // 経過時間をミリ秒で取得

    qDebug() << "処理にかかった時間 (QTime): " << elapsedMs << " ms";
    qDebug() << "--- QTimeでの処理時間測定終了 ---";

    return a.exec();
}

QElapsedTimerとの比較

  • QTimeは、日中の時刻(例: 午前10時30分)や時間の間隔(例: 5時間20分)を扱うのに適していますが、高精度なパフォーマンス測定にはQElapsedTimerが推奨されます。
  • QElapsedTimerはパフォーマンス測定に特化しており、システム時刻の変更に影響されない「単調増加クロック」を使用するため、より正確な経過時間測定に適しています。

C++ 標準ライブラリ (<chrono>)

C++11以降で導入された<chrono>ライブラリは、時間と期間を扱うための標準的で強力なツールです。プラットフォーム非依存で高精度な時間測定が可能です。

特徴

  • 期間の型安全性
    std::chrono::durationを使用して、期間の単位(ナノ秒、マイクロ秒、ミリ秒など)を型として扱えるため、誤った単位での計算を防ぎます。
  • 様々なクロック
    • std::chrono::high_resolution_clock: システムで利用可能な最高の精度を持つクロック。
    • std::chrono::steady_clock: 単調増加クロック(QElapsedTimerと同様に、システム時刻の変更に影響されない)。
    • std::chrono::system_clock: システムのウォールクロック(QTimeと同様に、システム時刻の変更に影響される)。
  • 高精度
    ナノ秒レベルの精度で時間を測定できます。
  • 標準的
    C++の標準ライブラリなので、Qtプロジェクトだけでなく、あらゆるC++プロジェクトで利用できます。

使用例

#include <QCoreApplication>
#include <chrono> // C++ 標準のchronoライブラリ
#include <QDebug>
#include <QThread>

void simulateSlowOperation() {
    QThread::msleep(200); // 200ミリ秒待機
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    qDebug() << "--- C++ chronoでの処理時間測定開始 ---";

    // high_resolution_clock を使用して開始時点を取得
    auto start = std::chrono::high_resolution_clock::now();

    simulateSlowOperation(); // 測定したい処理を実行

    // 終了時点を取得
    auto end = std::chrono::high_resolution_clock::now();

    // 期間を計算し、ミリ秒に変換
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    qDebug() << "処理にかかった時間 (chrono): " << duration.count() << " ms";
    qDebug() << "--- C++ chronoでの処理時間測定終了 ---";

    return a.exec();
}

QElapsedTimerとの比較

  • 使いやすさ
    QElapsedTimerはQtの他のクラスと連携しやすく、Qt開発者にとっては直感的かもしれません。
  • 機能の重複
    パフォーマンス測定という点では、QElapsedTimer<chrono>::steady_clockhigh_resolution_clockは機能が重複しています。どちらを使用するかは、プロジェクトがQt中心であるか、C++標準への依存を強くしたいかによります。
  • 移植性
    <chrono>はC++標準であるため、Qtに依存しないプロジェクトではより優れた移植性を提供します。

より低レベルで、OSが提供するAPIを直接使用して時間測定を行う方法もあります。これは通常、非常に特殊な要件(例えば、カーネルレベルの精度が必要な場合や、特定のハードウェアタイマーを使用したい場合)がある場合に限られます。


  • macOS
    mach_absolute_time
  • Linux
    clock_gettime (CLOCK_MONOTONIC または CLOCK_REALTIME)
  • Windows
    QueryPerformanceCounterQueryPerformanceFrequency

特徴

  • 複雑さ
    APIの呼び出しがQtやC++標準ライブラリよりも複雑になる傾向があります。
  • プラットフォーム依存
    コードが特定のOSに強く依存し、移植性が低くなります。
  • 最高の精度
    OSが提供する最も高精度なタイマーにアクセスできる場合があります。
  • QElapsedTimerは内部的にこれらのプラットフォーム固有のAPIを抽象化し、クロスプラットフォームで一貫した高精度なタイマー機能を提供しています。特別な理由がない限り、Qtアプリケーションで直接これらのAPIを使用する必要はほとんどありません。
  • 非常に特殊なケース
    究極の精度や特定のハードウェア制御が必要な場合にのみ、プラットフォーム固有のAPIを検討します。
  • 古いQtバージョンまたは単純な時刻計算
    Qt 4.7より前のバージョンを使用している場合や、システムの時刻変更に影響されても構わない簡単な時間間隔の計算には**QTime**も利用できます。
  • 汎用的なC++プロジェクト
    Qtに依存しないC++プロジェクトや、C++標準ライブラリに統一したい場合は、<chrono> が優れた選択肢です。
  • 最も推奨される方法
    Qtアプリケーションでパフォーマンス測定を行う場合は、QElapsedTimer が最も推奨されます。単調増加クロックを使用し、クロスプラットフォームで高精度な測定が可能です。