7

長い計算を実行するプログラムを書いています。これは、必要な数のタスクに分割できます。議論のために、2 から p-1 までのすべての数で割ることによって、数 p が素数かどうかを調べるアルゴリズムを書いているとしましょう。このタスクは明らかに多くのスレッドに分割できます。

実際に、まさにそれを行うサンプル アプリを作成しました。パラメーターとして、確認したい数値と使用するスレッドの数を指定します (各スレッドには、p を試して割るために、同じサイズの数値の範囲が与えられます。これらを合わせると、範囲全体がカバーされます)。

私のマシンには8つのコアがあります。私は素数 (2971215073) であることがわかっている多数のプログラムを実行し始め、1、2、3 スレッドなどで 8 スレッドに達するまで実行しました。しかし、8より大きい数を試してみると、計算時間は実際に(少しでも)短くなり続けました!

スレッドには I/O などはなく、純粋な CPU 計算のみです。コンテキストの切り替えが多くなり、並列実行スレッドの数が 8 のままになるため、8 スレッドを渡すとランタイムが悪化すると予想していました。違いが非常に小さく変化するため、ピークがどこにあるかを言うのは困難です。ただし、50 スレッドが 8 スレッドよりも速く (~300 ミリ秒) 実行されることは明らかです...

私の推測では、非常に多くのスレッドがあるため、システムのスレッド プールに大きな部分があるため、実行時間が長くなり、スレッドがより多く選択されるようになります。しかし、作成するスレッドが多いほどプログラムの実行速度が速くなるというのは理にかなっていないようです (そうでなければ、なぜ誰もが 1000 個のスレッドを作成しないのでしょうか??)。

マシンのコア数に対して作成するスレッドの数について、誰かが説明とおそらくベストプラクティスを提供できますか?

ありがとう。


興味のある人のための私のコード(Windows、VS2012でコンパイル):

#include <Windows.h>
#include <conio.h>
#include <iostream>
#include <thread>
#include <vector>

using namespace std;

typedef struct
{
    unsigned int primeCandidate;
    unsigned int rangeStart;
    unsigned int rangeEnd;
} param_t;


DWORD WINAPI isDivisible(LPVOID p)
{
    param_t* param = reinterpret_cast<param_t*>(p);

    for (unsigned int d = param->rangeStart; d < param->rangeEnd; ++d)
    {
        if (param->primeCandidate % d == 0)
        {
            cout << param->primeCandidate << " is divisible by " << d << endl;
            return 1;
        }
    }

    return 0;
}

bool isPrime(unsigned int primeCandidate, unsigned int numOfCores)
{
    vector<HANDLE> handles(numOfCores);
    vector<param_t> params(numOfCores);
    for (unsigned int i = 0; i < numOfCores; ++i)
    {
        params[i].primeCandidate = primeCandidate;
        params[i].rangeStart = (primeCandidate - 2) * (static_cast<double>(i) / numOfCores) + 2;
        params[i].rangeEnd = (primeCandidate - 2) * (static_cast<double>(i+1) / numOfCores) + 2;
        HANDLE h = CreateThread(nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(isDivisible), &params[i], 0, 0);
        if (NULL == h)
        {
            cout << "ERROR creating thread: " << GetLastError() << endl;
            throw exception();
        }
        handles[i] = h;
    }

    DWORD ret = WaitForMultipleObjects(numOfCores, &handles[0], TRUE, INFINITE);
    if (ret >= WAIT_OBJECT_0 && ret <= WAIT_OBJECT_0 + numOfCores - 1)
    {
        for (unsigned int i = 0; i < numOfCores; ++i)
        {
            DWORD exitCode = -1;
            if (0 == GetExitCodeThread(handles[i], &exitCode))
            {
                cout << "Failed to get thread's exit code: " << GetLastError() << endl;
                throw exception();
            }

            if (1 == exitCode)
            {
                return false;
            }
        }

        return true;
    }
    else
    {
        cout << "ERROR waiting on threads: " << ret << endl;
        throw exception();
    }
}

int main()
{
    unsigned int primeCandidate = 1;
    unsigned int numOfCores = 1;

    cout << "Enter prime candidate: ";
    cin >> primeCandidate;
    cout << "Enter # of cores (0 means all): ";
    cin >> numOfCores;
    while (primeCandidate > 0)
    {
        if (0 == numOfCores) numOfCores = thread::hardware_concurrency();

        DWORD start = GetTickCount();
        bool res = isPrime(primeCandidate, numOfCores);
        DWORD end = GetTickCount();
        cout << "Time: " << end-start << endl;
        cout << primeCandidate << " is " << (res ? "" : "not ") << "prime!" << endl;

        cout << "Enter prime candidate: ";
        cin >> primeCandidate;
        cout << "Enter # of cores (0 means all): ";
        cin >> numOfCores;
    }

    return 0;
}
4

2 に答える 2

5

はい。これは、i7/Vista 64 ボックスで行ったいくつかのテストの抜粋です (4 つの「実際の」コア + ハイパースレッディング)。

8 tests,
400 tasks,
counting to 10000000,
using 8 threads:
Ticks: 2199
Ticks: 2184
Ticks: 2215
Ticks: 2153
Ticks: 2200
Ticks: 2215
Ticks: 2200
Ticks: 2230
Average: 2199 ms

8 tests,
400 tasks,
counting to 10000000,
using 32 threads:
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2138
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2137
Average: 2137 ms

..テストのように、スレッドの「オーバーサブスクリプション」により、全体の実行時間がわずか2〜3%改善されることを示しています。私のテストでは、単純な「整数をカウントアップする」CPU 集中型タスクを、さまざまな数のスレッドを持つスレッドプールに送信しました。

当時の私の結論は、マイナーな改善は、より多くのスレッドが私のボックスの「基本負荷」のより大きな割合を占めるためであるというものでした.ほぼ常にアイドル状態の Firefox、uTorrent、Word、タスクバーなどは、テスト中にたまたま少し実行されました。

私のテストでは、たとえば 8 スレッドではなく 64 スレッドを使用することによる「コンテキスト切り替えのオーバーヘッド」は無視できる程度であり、無視できるようです。

これは、タスクで使用されるデータが非常に小さい場合にのみ適用されます。後で、タスクが 8K 配列 (L1 キャッシュのサイズ) を使用する同様の一連のテストを繰り返しました。この「最悪のケース」のシナリオでは、コアよりも多くのスレッドを使用すると、スレッドがキャッシュ全体をスワップインおよびスワップアウトするため、16 スレッド以上でパフォーマンスが 40% 低下するまで、非常に顕著な速度低下が生じました。約 20 スレッドを超えると、タスクを実行するスレッドの数に関係なく、キャッシュはすべてのコアから同じ速度でスワップアウトされるため、速度低下は悪化しませんでした。

また、十分な RAM があり、ページ フォールトがほとんどないことにも注意してください。

于 2013-06-15T13:27:52.037 に答える
1

各スレッドが実行する作業量が等しいと仮定していますが、実際にはそうではない場合があります。注目すべきは、各スレッドの終了時間です。それらの 1 つ以上が他のスレッドよりも大幅に早く終了している場合、スレッドを追加すると速度が向上することは理にかなっています。つまり、早期に停止すると、コアが使用されなくなることを意味します。余分なスレッドを使用することで、負荷がより公平に分散されます。

各スレッドの実行時間が異なる理由はいくつかあります。コードの基になる命令タイミングはわかりませんが、おそらく可変です。また、各スレッドには、分岐予測などの異なる CPU 最適化のセットがある可能性があります。OS に対してタイムスライスが失われるか、わずかなメモリ量で一時的に停止する可能性があります。一方が他方よりも遅くなる可能性がある多くの要因があると言えば十分です。

どれが最良の数かを言うのは難しい. 一般に、CPU の負荷を維持したいので、N 個のコアに対して N 個のスレッドについては一般的に正しいです。ただし、実際には追加のコアを持たないハイパースレッディングのようなことに注意してください。大量のメモリを使用しない限り、ハイパースレッディングは邪魔になります。AMDの新しいチップでは、FPUの数が半分であるため、整数命令は問題ありませんが、浮動小数点は停止する可能性があります.

各 CPU の負荷を維持したい場合、実際にそれを行う唯一の方法は、ジョブ ベースのフレームワークを使用することです。計算をより小さな単位に分割しますが (実際に行っているように)、それでもコアごとに 1 つのスレッドしかありません。スレッドは現在のジョブを完了すると、次の利用可能なジョブを取得する必要があります。このように、一部のジョブが長い/短いかどうかは関係ありません。解放された CPU は次のジョブに移動します。

もちろん、これは計算が長い場合にのみ意味があります。合計時間がわずか数秒の場合、ジョブのオーバーヘッドにより若干速度が低下する可能性があります。しかし、4 ~ 5 秒から始めても、効果が見られるはずです。また、小規模なタイミング テストを行う場合は、CPU 周波数スケーリングをオフにしてください。そうしないと、各 CPU の速度アップ/ダウン時間が基本的にランダムな結果になります。

于 2013-06-15T14:45:45.530 に答える