スレッドコンテキスト切り替えのオーバーヘッドを見積もる方法は?

既存のプラットフォームの Web 上のどこかでこのオーバーヘッドを見つけることができるとは思えません。あまりにも多くの異なるプラットフォームが存在します。オーバーヘッドは次の 2 つの要因によって異なります:

  • CPU。必要な操作は、CPU の種類によって簡単な場合もあれば難しい場合もあります
  • システム カーネル。異なるカーネルは各スイッチで異なる操作を実行する必要があります

他の要因には、切り替えがどのように行われるかが含まれます。

<オール> <リ>

スレッドは、そのタイム クォンタムをすべて使用しました。スレッドが開始されると、次のスレッドを決定するカーネルに制御を戻す前に、一定時間実行される場合があります。

<リ>

スレッドがプリエンプトされました。これは、別のスレッドが CPU 時間を必要とし、優先度が高い場合に発生します。例えば。マウス/キーボード入力を処理するスレッドは、そのようなスレッドである可能性があります。 所有するスレッドに関係なく ユーザーが何かを入力したり何かをクリックしたりしたとき、現在のスレッドの時間量が完全に使い果たされるまで待ちたくありません。彼は、システムがすぐに反応するのを見たいと思っています。したがって、システムによっては、現在のスレッドをすぐに停止させ、より優先度の高い他のスレッドに制御を戻します。

<リ>

スレッドは、何らかの操作でブロックされているか、実行を停止するために単に sleep() (または同様の) を呼び出しているため、CPU 時間を必要としません。

これら 3 つのシナリオでは、理論上、スレッドの切り替え時間が異なる場合があります。例えば。 sleep() への呼び出しは CPU がカーネルに返され、カーネルがウェイクアップ コールをセットアップして、スレッドが約スリープを要求した時間の長さを超えると、そのスレッドをスケジューリング プロセスから外す必要があり、スレッドが起動したら、そのスレッドをスケジューリング プロセスに再度追加する必要があります。これらすべての急勾配には、ある程度の時間がかかります。そのため、実際のスリープ コールは、別のスレッドに切り替えるのにかかる時間よりも長くなる可能性があります。

確実に知りたい場合は、ベンチマークする必要があると思います。問題は、通常、スレッドをスリープ状態にするか、ミューテックスを使用してスレッドを同期する必要があることです。ミューテックスのスリープまたはロック/ロック解除には、それ自体にオーバーヘッドがあります。これは、ベンチマークにこれらのオーバーヘッドも含まれることを意味します。強力なプロファイラーがなければ、実際の切り替えにどれだけの CPU 時間が使用され、スリープ/ミューテックス呼び出しにどれだけの時間が費やされたかを後で判断するのは困難です。一方、実際のシナリオでは、スレッドはスリープするか、ロックを介して同期します。コンテキスト切り替え時間を純粋に測定するベンチマークは、実際のシナリオをモデル化していないため、総合的なベンチマークです。ベンチマークは、実際のシナリオに基づいている場合、より「現実的」です。実際の 3D アプリケーションでこの結果を達成できない場合、GPU が理論上 1 秒間に 20 億ポリゴンを処理できることを示す GPU ベンチマークは何の役に立つでしょうか。 GPU が 1 秒間に処理できる実際の 3D アプリケーションのポリゴン数を知ることは、もっと興味深いことではないでしょうか?

残念ながら、私は Windows プログラミングについて何も知りません。 Java や C# で Windows 用のアプリケーションを作成することもできましたが、Windows での C/C++ は泣かせます。 POSIX 用のソース コードしか提供できません。

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>

uint32_t COUNTER;
pthread_mutex_t LOCK;
pthread_mutex_t START;
pthread_cond_t CONDITION;

void * threads (
    void * unused
) {
    // Wait till we may fire away
    pthread_mutex_lock(&START);
    pthread_mutex_unlock(&START);

    pthread_mutex_lock(&LOCK);
    // If I'm not the first thread, the other thread is already waiting on
    // the condition, thus Ihave to wake it up first, otherwise we'll deadlock
    if (COUNTER > 0) {
        pthread_cond_signal(&CONDITION);
    }
    for (;;) {
        COUNTER++;
        pthread_cond_wait(&CONDITION, &LOCK);
        // Always wake up the other thread before processing. The other
        // thread will not be able to do anything as long as I don't go
        // back to sleep first.
        pthread_cond_signal(&CONDITION);
    }
    pthread_mutex_unlock(&LOCK); //To unlock
}

int64_t timeInMS ()
{
    struct timeval t;

    gettimeofday(&t, NULL);
    return (
        (int64_t)t.tv_sec * 1000 +
        (int64_t)t.tv_usec / 1000
    );
}


int main (
    int argc,
    char ** argv
) {
    int64_t start;
    pthread_t t1;
    pthread_t t2;
    int64_t myTime;

    pthread_mutex_init(&LOCK, NULL);
    pthread_mutex_init(&START, NULL);   
    pthread_cond_init(&CONDITION, NULL);

    pthread_mutex_lock(&START);
    COUNTER = 0;
    pthread_create(&t1, NULL, threads, NULL);
    pthread_create(&t2, NULL, threads, NULL);
    pthread_detach(t1);
    pthread_detach(t2);
    // Get start time and fire away
    myTime = timeInMS();
    pthread_mutex_unlock(&START);
    // Wait for about a second
    sleep(1);
    // Stop both threads
    pthread_mutex_lock(&LOCK);
    // Find out how much time has really passed. sleep won't guarantee me that
    // I sleep exactly one second, I might sleep longer since even after being
    // woken up, it can take some time before I gain back CPU time. Further
    // some more time might have passed before I obtained the lock!
    myTime = timeInMS() - myTime;
    // Correct the number of thread switches accordingly
    COUNTER = (uint32_t)(((uint64_t)COUNTER * 1000) / myTime);
    printf("Number of thread switches in about one second was %u\n", COUNTER);
    return 0;
}

出力

Number of thread switches in about one second was 108406

100,000 を超えることはそれほど悪くありません。ロックと条件付きの待機がありますが。このすべてがなければ、1 秒間に少なくとも 2 倍の回数のスレッド切り替えが可能だったと思います。


あなたはそれを見積もることはできません。あなたはそれを測定する必要があります。また、デバイスのプロセッサによって異なります。

コンテキスト スイッチを測定するには、2 つの非常に簡単な方法があります。 1 つはコードを含み、もう 1 つはコードを含みません。

まず、コードの方法 (疑似コード):

DWORD tick;

main()
{
  HANDLE hThread = CreateThread(..., ThreadProc, CREATE_SUSPENDED, ...);
  tick = QueryPerformanceCounter();
  CeSetThreadPriority(hThread, 10); // real high
  ResumeThread(hThread);
  Sleep(10);
}

ThreadProc()
{
  tick = QueryPerformanceCounter() - tick;
  RETAILMSG(TRUE, (_T("ET: %i\r\n"), tick));
}

明らかにループでそれを行い、平均化する方が良いでしょう。これは、コンテキスト スイッチを測定するだけではないことに注意してください。また、ResumeThread の呼び出しも測定していますが、スケジューラがすぐに別のスレッドに切り替わるという保証はありません (優先度 10 は、そうなる可能性を高めるのに役立ちます)。

スケジューラ イベントにフックすることで、CeLog を使用してより正確な測定値を得ることができますが、実行するのは簡単ではなく、十分に文書化されていません。本当にそのルートに行きたい場合は、Sue Loh のブログがいくつかあり、検索エンジンで見つけることができます。

非コード ルートは、Remote Kernel Tracker を使用することです。 eVC 4.0 または Platform Builder の評価版をインストールして入手します。カーネルが行っているすべてのことをグラフィカルに表示し、提供されたカーソル機能を使用してスレッド コンテキスト スイッチを直接測定できます。繰り返しますが、Sue も Kernel Tracker の使用に関するブログ エントリを持っていると確信しています。

とはいえ、CE のプロセス内スレッド コンテキスト スイッチは非常に高速であることがわかります。 RAM でアクティブなプロセスをスワップしてから移行を行う必要があるため、コストがかかるのはプロセスの切り替えです。


あなたはテスト アプリケーションを書きたくないと言っていましたが、私は ARM9 Linux プラットフォームでの以前のテストで、オーバーヘッドが何であるかを調べるためにこれを行いました。 boost::thread::yield() (または、ご存知のように) 変数をインクリメントするのは 2 つのスレッドだけでした。 1 秒あたりに実行できるコンテキスト スイッチの数。もちろん、これは正確ではありませんが、ポイントは、両方のスレッドが互いに CPU を譲り合ったということです。あまりにも高速だったので、オーバーヘッドについて考えることはもはや意味がありませんでした。存在しないかもしれない問題について考えすぎるのではなく、簡単なテストを書きましょう。

それ以外は、パフォーマンス カウンターで提案されている 1800 のように試すことができます。

ああ、Windows CE 4.X で実行されているアプリケーションを覚えています。ここでも 4 つのスレッドが頻繁に切り替えられ、パフォーマンスの問題に遭遇することはありませんでした。また、スレッドをまったく使用せずにコアのスレッド化を実装しようとしましたが、パフォーマンスの向上は見られませんでした (GUI の応答が大幅に遅くなりましたが、他のすべては同じでした)。おそらく、コンテキスト スイッチの数を減らすか、スレッドを完全に削除することで、同じことを試すことができます (テストのためだけに)。