Qt開発者のためのQElapsedTimer活用術:サンプルコードで学ぶ高精度タイマー

2025-05-27

QElapsedTimer (クラス) とは

QElapsedTimerは、Qtフレームワークが提供するクラスで、経過時間を高速かつ正確に計測するために使用されます。特に、特定の操作にかかった時間を測定したり、一定の時間内に処理を完了する必要がある場合などに役立ちます。

QTimeクラスも時間の計測に使用できますが、QElapsedTimerには以下の重要な違いと利点があります。

  1. モノトニッククロックの使用:

    • QElapsedTimerは、可能な限り**モノトニッククロック(単調増加クロック)**を使用しようとします。モノトニッククロックとは、システムの時刻がユーザーによって変更されたり、サマータイムによって自動的に調整されたりしても、常に前方へ進む(逆戻りしない)性質を持つクロックです。
    • これにより、QElapsedTimerで計測された時間は、システム時刻の変更に影響されず、より信頼性の高い経過時間を提供します。
    • 一方で、QElapsedTimerで得られた時間は、一般的な「人間が読める時刻」(例:午後3時25分)に変換することはできません。これは、単に時間の間隔を測定するためのものです。
  2. パフォーマンスと精度:

    • QElapsedTimerは、システムが提供する最も高精度なクロック(例えば、Windowsの高性能カウンターやmacOSのMach Absolute Timeなど)を利用して、高速かつ高精度な時間計測を行います。
    • デバッグ目的で特定の処理にかかる時間を計測したり、タイムアウト処理を実装したりするのに適しています。

主な機能と使い方

QElapsedTimerの使い方は非常にシンプルです。

  1. タイマーの開始: start()メソッドを呼び出すことでタイマーを開始します。

    QElapsedTimer timer;
    timer.start(); // タイマーを開始
    
  2. 経過時間の取得: elapsed()メソッドを呼び出すことで、タイマーが開始されてからの経過時間をミリ秒単位で取得できます。

    // ... 何らかの時間のかかる処理 ...
    qint64 milliseconds = timer.elapsed(); // 経過時間をミリ秒で取得
    qDebug() << "処理にかかった時間:" << milliseconds << "ミリ秒";
    

    ナノ秒単位で経過時間を取得したい場合は、nsecsElapsed()を使用します。

  3. タイマーのリスタート: restart()メソッドは、タイマーをリスタートし、同時に前回のスタートからの経過時間を返します。

    qint64 elapsedSinceLastStart = timer.restart(); // タイマーをリスタートし、前回のスタートからの経過時間を取得
    
  4. タイムアウトのチェック: hasExpired(qint64 timeout)メソッドを使用すると、指定されたミリ秒数(timeout)が経過したかどうかを簡単にチェックできます。

    void doSomethingForAWhile(int maxDurationMs) {
        QElapsedTimer timer;
        timer.start();
        while (!timer.hasExpired(maxDurationMs)) {
            // 時間内に実行したい処理
            // 例: 他の処理を呼び出す
        }
        qDebug() << "指定された時間が経過しました。";
    }
    
特徴QElapsedTimerQTime
用途経過時間の正確な計測、パフォーマンス測定、タイムアウト処理時刻の表現、時間計算(例:特定時刻からの差分)
基準クロックモノトニッククロック (推奨)システム時刻(ユーザーやサマータイムで変更される可能性あり)
精度高精度(システムが提供する最高の精度)通常ミリ秒単位
人間の可読性なし(単なる時間の間隔)あり(日付・時刻として表示可能)
オーバーフロー内部的に処理されるため、基本的には心配不要一部のプラットフォームで32ビットカウンタの場合、約49.7日でオーバーフローする可能性あり


    • 問題: QElapsedTimerはモノトニッククロックを使用するため、通常はシステム時刻の変更に影響されません。しかし、QElapsedTimerの値をQDateTimeなど他の時間クラスと組み合わせて「人間の読める時刻」を計算しようとすると、期待する結果にならないことがあります。
    • 説明: QElapsedTimerは純粋に「スタート地点からどれだけ時間が経過したか」を測るものであり、特定の絶対時刻(例:2023年10月27日15時30分)を示すものではありません。
    • :
      QElapsedTimer timer;
      timer.start();
      // ここでPCのシステム時刻を手動で変更したとする
      QThread::sleep(5); // 5秒待機
      qint64 elapsed = timer.elapsed(); // 5秒が返る(システム時刻変更の影響を受けない)
      
      // 誤った使い方(タイマーの経過時間から絶対時刻を推測しようとする)
      // このようにQElapsedTimerの経過時間を使ってQDateTimeを「補正」しようとすると、
      // システム時刻の変更を考慮しないため、意図しない結果になることがあります。
      // 例えば、開始時のQDateTime::currentDateTime()からelapsedを足し算しても、
      // その間にシステム時刻が変更されていればズレが生じます。
      
    • トラブルシューティング:
      • QElapsedTimerは「経過時間」の計測に特化していることを理解してください。
      • 絶対時刻が必要な場合はQDateTimeQDateQTimeを使用し、それらのクラスをQElapsedTimerと混同しないようにしてください。
      • 経過時間と絶対時刻の両方が必要な場合は、それぞれのクラスを適切に使い分け、必要に応じて両方の情報を記録してください。
  1. start() または restart() の呼び忘れ

    • 問題: elapsed()nsecsElapsed()を呼び出す前にstart()またはrestart()を呼び出していない場合、タイマーは開始されておらず、結果がゼロになったり、予期せぬ値になったりします。
    • 説明: QElapsedTimerは、明示的に開始されるまで計測を開始しません。
    • :
      QElapsedTimer timer;
      // timer.start(); // これがないと計測が始まらない
      QThread::sleep(1);
      qint64 ms = timer.elapsed(); // 0または不定な値が返る可能性がある
      qDebug() << ms;
      
    • トラブルシューティング:
      • elapsed()nsecsElapsed()を呼び出す前に、必ずstart()またはrestart()を呼び出していることを確認してください。
      • デバッグ時に、タイマーが適切に開始されているかを確認するために、timer.isValid()(タイマーが有効なクロックを保持しているか)や、timer.isStarted()(タイマーが開始されているか)をチェックすることも有効です。
  2. 非常に短い時間の計測と精度の限界

    • 問題: ミリ秒以下の非常に短い時間を計測しようとすると、期待通りの精度が得られない場合があります。
    • 説明: QElapsedTimerはシステムが提供する最高の精度を利用しますが、それでもハードウェアやOSのクロック解像度には限界があります。特に、コンテキストスイッチ、キャッシュミスの影響、他のプロセスによるCPU使用率の変動などが、非常に短い計測に影響を与えることがあります。
    • : 非常に高速なループ内の各イテレーションの時間を計測しようとすると、計測オーバーヘッドが実際の処理時間よりも大きくなることがあります。
    • トラブルシューティング:
      • 非常に短い時間の計測を行う場合は、複数回処理を実行して平均を取る(例:1000回ループさせて合計時間を計測し、それを回数で割る)ことで、より信頼性の高い結果を得られます。
      • nsecsElapsed()はより高精度ですが、ナノ秒単位の計測が本当に必要かつ信頼できるか、システムのクロック解像度を考慮してください。
      • プロファイリングツール(Valgrind, Qt Creatorのプロファイラーなど)の方が、特定のコードブロックのパフォーマンス問題を特定するには適している場合があります。
  3. タイマーの「リセット」忘れによる累積時間の誤解

    • 問題: 同じQElapsedTimerインスタンスを複数の異なる計測に使用する際に、restart()を呼び忘れると、前回の計測からの累積時間が返されてしまいます。
    • 説明: elapsed()は、最後にstart()またはrestart()が呼び出されてからの経過時間を返します。
    • :
      QElapsedTimer timer;
      
      // 最初の処理
      timer.start();
      QThread::sleep(1);
      qDebug() << "Process A took:" << timer.elapsed() << "ms"; // 例: 1000ms
      
      // 次の処理(ここで本来はtimer.restart()が必要)
      QThread::sleep(2);
      qDebug() << "Process B took:" << timer.elapsed() << "ms"; // 例: 3000ms (1000 + 2000) と表示される
      
    • トラブルシューティング:
      • 異なる処理や異なる計測フェーズでタイマーを再利用する場合は、必ずrestart()を呼び出してタイマーをリセットし、新しい計測を開始してください。
      • restart()は現在の経過時間を返しつつ、タイマーをリセットするので、計測とリセットを同時に行いたい場合に便利です。
  4. スレッド間のQElapsedTimerの共有

    • 問題: 異なるスレッド間で同じQElapsedTimerインスタンスを共有し、同時に操作すると、競合状態が発生し、結果が不正確になったり、クラッシュしたりする可能性があります。
    • 説明: QElapsedTimerはスレッドセーフではありません。
    • トラブルシューティング:
      • 各スレッドで独自のQElapsedTimerインスタンスを作成し、使用してください。
      • もし何らかの理由でスレッド間でタイマーの状態を共有する必要がある場合は、QMutexなどのスレッド同期プリミティブを使用して、タイマーへのアクセスを保護する必要があります。しかし、通常は個々のスレッドでタイマーを独立して使用する方がシンプルで安全です。


QElapsedTimerは、アプリケーションの特定の処理にかかる時間を計測したり、タイムアウト処理を実装したりするのに非常に便利です。ここではいくつかの典型的な使用例を紹介します。

例1: 単純な処理時間の計測

最も基本的な使い方で、ある関数やコードブロックの実行にかかる時間をミリ秒単位で計測します。

#include <QCoreApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QThread> // QThread::sleep() のために必要

// 時間がかかることをシミュレートする関数
void simulateLongRunningTask(int milliseconds)
{
    qDebug() << "タスク開始: " << milliseconds << "ミリ秒間スリープします...";
    QThread::msleep(milliseconds); // 指定されたミリ秒間スリープ
    qDebug() << "タスク終了。";
}

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

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

    // --- 最初のタスクの計測 ---
    qDebug() << "--- 最初のタスク計測開始 ---";
    timer.start(); // タイマーを開始
    simulateLongRunningTask(1500); // 1.5秒かかるタスクをシミュレート
    qint64 elapsed1 = timer.elapsed(); // 経過時間をミリ秒で取得
    qDebug() << "最初のタスクにかかった時間: " << elapsed1 << "ミリ秒";
    qDebug() << "--- 最初のタスク計測終了 ---\n";

    // --- 次のタスクの計測 (restart() を使用) ---
    // restart() はタイマーをリセットし、同時に前回のスタートからの経過時間を返します
    qDebug() << "--- 次のタスク計測開始 (restart() 使用) ---";
    qint64 elapsedSinceLastStart = timer.restart(); // タイマーをリスタートし、前回の経過時間を取得
    qDebug() << "restart() 呼び出しまでの時間 (前回のタスクの残り時間): " << elapsedSinceLastStart << "ミリ秒";
    simulateLongRunningTask(2000); // 2秒かかるタスクをシミュレート
    qint64 elapsed2 = timer.elapsed(); // リスタートしてからの経過時間を取得
    qDebug() << "次のタスクにかかった時間: " << elapsed2 << "ミリ秒";
    qDebug() << "--- 次のタスク計測終了 ---\n";

    // --- 非常に短いタスクの計測 (nsecsElapsed() を使用) ---
    qDebug() << "--- 短いタスク計測開始 (nsecsElapsed() 使用) ---";
    timer.start();
    // 非常に短い操作をシミュレート (例えば、単純な計算など)
    volatile int sum = 0; // 最適化で消されないようにvolatile
    for (int i = 0; i < 10000; ++i) {
        sum += i;
    }
    qint64 elapsedNanos = timer.nsecsElapsed(); // 経過時間をナノ秒で取得
    qDebug() << "短いタスクにかかった時間 (合計): " << elapsedNanos << "ナノ秒";
    qDebug() << "--- 短いタスク計測終了 ---\n";

    return a.exec(); // イベントループを開始
}

出力例:

--- 最初のタスク計測開始 ---
タスク開始:  1500 ミリ秒間スリープします...
タスク終了。
最初のタスクにかかった時間:  1500 ミリ秒
--- 最初のタスク計測終了 ---

--- 次のタスク計測開始 (restart() 使用) ---
restart() 呼び出しまでの時間 (前回のタスクの残り時間):  1500 ミリ秒
タスク開始:  2000 ミリ秒間スリープします...
タスク終了。
次のタスクにかかった時間:  2000 ミリ秒
--- 次のタスク計測終了 ---

--- 短いタスク計測開始 (nsecsElapsed() 使用) ---
短いタスクにかかった時間 (合計):  XXXX ナノ秒 (XXXX は環境によって異なる)
--- 短いタスク計測終了 ---

例2: タイムアウト処理の実装

QElapsedTimerは、特定の処理が指定された時間内に完了するかどうかをチェックするタイムアウト処理にも使えます。

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

// 時間のかかる処理をシミュレートする関数
bool performOperationWithinTimeout(int maxDurationMs)
{
    QElapsedTimer timer;
    timer.start();

    qDebug() << "タイムアウト: " << maxDurationMs << "ミリ秒以内で操作を試みます...";

    // 模擬的な処理ループ
    int iterations = 0;
    while (!timer.hasExpired(maxDurationMs)) {
        // ここに時間のかかる実際の処理を記述
        // 例えば、ネットワーク通信、ファイルI/O、複雑な計算など
        QThread::msleep(100); // 100ミリ秒ごとに処理の一部をシミュレート
        iterations++;

        if (iterations * 100 > maxDurationMs / 2 && iterations * 100 < maxDurationMs - 100) {
            // 例: 特定の条件下で処理が早めに完了すると仮定
            if (qrand() % 100 < 20) { // 20%の確率で早めに完了
                qDebug() << "操作が早めに完了しました!経過時間: " << timer.elapsed() << "ms";
                return true; // タイムアウト前に完了
            }
        }
    }

    qDebug() << "タイムアウト! 指定された時間内に操作が完了しませんでした。経過時間: " << timer.elapsed() << "ms";
    return false; // タイムアウト
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qsrand(QDateTime::currentMSecsSinceEpoch()); // 乱数のシードを設定

    qDebug() << "\n--- シナリオ1: タイムアウトしないケース (余裕がある) ---";
    bool success1 = performOperationWithinTimeout(2000); // 2秒のタイムアウト
    qDebug() << "操作結果 (シナリオ1): " << (success1 ? "成功" : "失敗");

    qDebug() << "\n--- シナリオ2: タイムアウトするケース (時間不足) ---";
    bool success2 = performOperationWithinTimeout(500); // 0.5秒のタイムアウト
    qDebug() << "操作結果 (シナリオ2): " << (success2 ? "成功" : "失敗");

    return a.exec();
}

出力例 (実行ごとに「操作が早めに完了しました!」の有無が変わる可能性があります):

--- シナリオ1: タイムアウトしないケース (余裕がある) ---
タイムアウト:  2000 ミリ秒以内で操作を試みます...
操作が早めに完了しました!経過時間:  XXX ms  // 例: 500ms
操作結果 (シナリオ1):  成功

--- シナリオ2: タイムアウトするケース (時間不足) ---
タイムアウト:  500 ミリ秒以内で操作を試みます...
タイムアウト! 指定された時間内に操作が完了しませんでした。経過時間:  500 ms
操作結果 (シナリオ2):  失敗

例3: ループの平均実行時間の計測

QElapsedTimerは、短いループ内の各イテレーションの平均時間を計測するのにも適しています。

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

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

    const int numIterations = 1000000; // 100万回ループ

    QElapsedTimer timer;
    timer.start();

    // 非常に簡単な計算をループで実行
    volatile double result = 0.0; // 最適化で消されないようにvolatile
    for (int i = 0; i < numIterations; ++i) {
        result += qSin(static_cast<double>(i) * 0.0001); // 少し計算負荷のある操作
    }

    qint64 totalElapsedNanos = timer.nsecsElapsed(); // 全体の経過時間をナノ秒で取得

    qDebug() << "合計イテレーション数: " << numIterations;
    qDebug() << "合計経過時間: " << totalElapsedNanos << "ナノ秒";

    if (numIterations > 0) {
        double averageNanosPerIteration = static_cast<double>(totalElapsedNanos) / numIterations;
        qDebug() << "1イテレーションあたりの平均時間: " << averageNanosPerIteration << "ナノ秒";
    }

    return a.exec();
}

出力例:

合計イテレーション数:  1000000
合計経過時間:  XXXXXXX ナノ秒 (XXXXXXX は環境によって異なる)
1イテレーションあたりの平均時間:  YYY.ZZZ ナノ秒 (YYY.ZZZ は環境によって異なる)


QTime クラス

QTimeはQtが提供する最も基本的な時間クラスの一つで、日付情報を持たずに時刻(時、分、秒、ミリ秒)を扱います。経過時間計測にも使用できますが、QElapsedTimerとはいくつか重要な違いがあります。

  • 使い分け:

    • システム時刻の変更が問題にならない、または絶対時刻と連動したシンプルな計測が必要な場合。
    • デバッグ目的での大まかな時間計測。
  • 欠点:

    • システム時刻の変更に影響される: PCのシステム時刻が変更されると、計測結果が狂う可能性があります(サマータイムの調整、ユーザーによる手動変更など)。これがQElapsedTimerとの最大の相違点です。
    • 精度がQElapsedTimerほど高くない場合があります(通常はミリ秒単位まで)。
    • restart()hasExpired()のような便利なメソッドはありません。
  • 利点:

    • シンプルで分かりやすいAPI。
    • 特定の絶対時刻(例:今日の午前9時)を扱うのに適している。
  • 使い方: start()メソッドで開始し、elapsed()メソッドで経過ミリ秒を取得します。

    #include <QTime>
    #include <QDebug>
    #include <QThread>
    
    void exampleQTime() {
        QTime timer;
        timer.start(); // タイマーを開始
    
        QThread::msleep(1500); // 1.5秒待機
    
        qint64 elapsedMs = timer.elapsed(); // 経過時間をミリ秒で取得
        qDebug() << "QTime 経過時間: " << elapsedMs << "ミリ秒";
    
        // 特定の絶対時刻からの経過時間取得にも使えます
        // QTime now = QTime::currentTime();
        // int msecsSinceStartOfDay = now.msecsSinceStartOfDay();
    }
    

C++ 標準ライブラリの <chrono> (C++11 以降)

C++11以降の標準ライブラリには、時間に関する強力な機能を提供する<chrono>ヘッダがあります。これを使用すると、プラットフォームに依存しない高精度な時間計測が可能です。

  • 使い分け:

    • Qtの依存を避けたい、または純粋なC++標準ライブラリのソリューションを好む場合。
    • クロスプラットフォームかつ高精度な時間計測が必要な場合。
  • 欠点:

    • QElapsedTimerのようなstart()elapsed()といったシンプルなメソッドチェーンではなく、開始と終了のタイムポイントを記録し、その差分を計算するというやや冗長な記述になる。
    • タイムアウト処理のhasExpired()のような直接的なメソッドは提供されないため、自分で実装する必要がある。
  • 利点:

    • プラットフォーム非依存: Qtに依存しないため、Qtを使用しないC++プロジェクトでも利用可能。
    • 高精度: システムが提供する最高の精度を利用可能。
    • モノトニッククロック: std::chrono::steady_clockはシステム時刻の変更に影響されない。
    • 柔軟な時間単位変換。
  • 使い方: std::chrono::high_resolution_clockstd::chrono::steady_clock を使用します。steady_clock はモノトニッククロックであり、QElapsedTimerに最も近い挙動をします。high_resolution_clock は最も高精度なクロックですが、モノトニックではない可能性もあります(実装依存)。通常はsteady_clockが推奨されます。

    #include <chrono>
    #include <QDebug>
    #include <QThread>
    
    void exampleChrono() {
        // steady_clock はモノトニッククロックで、QElapsedTimer に近い
        auto start = std::chrono::steady_clock::now(); // 開始時間を記録
    
        QThread::msleep(1500); // 1.5秒待機
    
        auto end = std::chrono::steady_clock::now(); // 終了時間を記録
    
        // 経過時間を計算 (例: ミリ秒単位)
        std::chrono::duration<double, std::milli> duration = end - start;
        qDebug() << "std::chrono::steady_clock 経過時間: " << duration.count() << "ミリ秒";
    
        // ナノ秒単位で取得することも可能
        // std::chrono::duration<long long, std::nano> nanos = end - start;
        // qDebug() << "経過時間: " << nanos.count() << "ナノ秒";
    }
    

プラットフォーム固有のAPI

OSによっては、独自の高精度タイマーAPIを提供している場合があります。

  • 使い分け:

    • 非常に特殊なケースで、Qtや標準ライブラリの機能では満たせないほどの究極のパフォーマンスや低レベル制御が必要な場合。しかし、ほとんどのQtアプリケーションでは不要です。
  • 欠点:

    • プラットフォーム依存: コードの移植性が低下する。
    • Qtの抽象化されたインターフェースに比べて、使用が複雑になる場合が多い。
  • 利点:

    • 特定のプラットフォームで最高のパフォーマンスと精度が期待できる。
  • 使い方: これらのAPIはOSごとに異なります。Qtは内部的にこれらのAPIをラップしてQElapsedTimerを提供しているため、通常は直接これらを使用する必要はありません。

    // Windows の例 (概念のみ、実際にはヘッダやライブラリのリンクが必要)
    // #include <windows.h>
    // LARGE_INTEGER start, end, frequency;
    // QueryPerformanceFrequency(&frequency);
    // QueryPerformanceCounter(&start);
    // // ... 処理 ...
    // QueryPerformanceCounter(&end);
    // double elapsedMs = static_cast<double>(end.QuadPart - start.QuadPart) * 1000.0 / frequency.QuadPart;
    
  • macOS: mach_absolute_time

  • Linux: clock_gettime (通常 CLOCK_MONOTONIC を使用)

  • Windows: QueryPerformanceCounter / QueryPerformanceFrequency

代替手段目的/特徴利点欠点
QTimeシンプルな経過計測、絶対時刻との関連APIがシンプル、Qtエコシステムに統合システム時刻変更に弱い、精度が限定的
<chrono>高精度な経過計測、プラットフォーム非依存モノトニッククロック利用可能、高精度、標準C++APIが冗長になりがち、QElapsedTimerのような便利メソッドがない
プラットフォーム固有特定OSでの最高精度・性能特定OSでの究極の最適化プラットフォーム依存、移植性なし、使用が複雑