汎用ハードウェア(通常のデスクトップPCやサーバーPCなど)の合計実行時間を最小化する場合、アルゴリズムをbestと定義します。
[〜#〜] a [〜#〜]と[〜#〜] b [〜#〜]のセットがあります。 f(a、b、n)のように機能する関数fもあります。ここで、aは[〜 #〜] a [〜#〜]、bは[〜#〜] b [〜#〜]、およびnは自然数です。 this f(a、b、n)が返す出力は、正の実数です。
これらのプロパティは正しいです:
私の目標は、次のようにanswerの値を計算することです。
count = 0
output = 0
for a in A:
for b in B:
output += f(a,b,n)
count += 1
answer = output / count
return answer
ただし、上記のコードは高度にシリアル化されています。私がしたいのは、それを並列化して、回答answerを取得するために必要な合計秒数を最小化することです。
繰り返しになりますが、私はこのアプリケーションを1台のコンピューターで実行しているため、マルチスレッドアプリケーションを検討しています。複数のマシンに配布したくありません。私が欲しいのは、1台のコンピューターにある多くのコア上で本当に高速に実行することだけです。
この答えは Erik Eidtの答え の上に構築されています。
データをスライスする方法は3つあります。Aのサブレンジ、Bのサブレンジ、Nのサブレンジです。後者は、すでに独自の回答で行っています。
セットの1つをスライスするだけでなく、AとBの両方でスライスすることもできます。
説明する前に、数学表記を使用していないことを最初に謝罪しなければなりません。 このサイトはMathJaxをサポートしていません したがって、複数レベルの添え字を含む表記法を書くことはできません。
複数のサブセット(またはデータの次元)でスライスするための名前は ループタイリング です。
簡単にするために、次のようにAをスライスしてみましょう:(A1... A10)、(A11... A20)、(A21... A30)、...、および同様に次のようにB:(B1... B10)、(B11... B20)、(B21... B30)
AとBを10個のグループにスライスする選択は任意です。より最適な組み合わせを見つけるには、さまざまなグループ化サイズを試してみてください。
疑似コードは次のとおりです。
for each ten consecutive items taken from A, namely A(10*j+1...10*j+9)
for each ten consecutive items taken from B, namely B(10*k+1...10*k+9)
for each item in the subrange of A(10*j+1...10*j+9), namely "a"
for each item in the subrange of B(10*k+1...10*k+9), namely "b"
for i=1...n
process f(a, b, ...)
「ループタイリング」手法の詳細を強調するために、擬似コードでは一部の詳細が省略されています。
並列化の側面は省略しましたが、基本的には「for」のいずれかを選択して「parallel for」に変換し、マルチコアの使用率を完全に最大化することができます。
より一般的なアドバイス。
上記のリンクされたWikipediaの記事で説明されているように、AとBに最適なサブレンジサイズを選択するのに役立つ簡単なルールはありません。代わりに、さまざまな値を試して、どの組み合わせがより速く実行されるかを確認する必要があります。パフォーマンスの実験から、どのソフトウェアパフォーマンス要因が支配的であるかを発見できます。これは発見することしかできず、理論だけで予測するのは簡単ではありません。
Cを使用しているため、いくつかの選択肢があります。
タグにpthreadがあることがわかります。ただし、このタイプの計算では、代わりにOpenMPを使用する方がパフォーマンスが向上します。
スレッドセーフ についてはすでに知っているはずです。そうしないと、最初からマルチスレッドプログラムがどれも正しい答えを出せない可能性があります。したがって、それが何であるかを理解するか、複数のスレッドを使用しないようにすることが非常に重要です。
マルチスレッドなしでマルチコアCPU使用率を高める1つの方法は、複数のプロセス(OSプロセス)を使用することです。つまり、複数のコンソール端末を開き、各端末でプログラムを起動します。出力を異なるファイル名で保存するように各プログラムに指示します。すべてのプログラムが終了したら、これらの異なるファイルを組み合わせて最終結果を作成します。
ここで作業することは多くありません。 f
を漏らしたり、f
の内部を並列化するように要求したりするのではなく、f
を呼び出す二重にネストされたループだけです。 f(, n)とf(, n-1)の間にはいくつかの関係がありますが、開示されていないランダム化コンポーネントのため、それを利用する方法はわかりません。
(これはあなたの意図だと思いますが、明確にするために、ランダム化コンポーネントを理解できれば、より良い解決策があるかもしれません。それを何度も繰り返すと、すべての作業が実際に行われているように見え、代わりに別のことを行う可能性があるためです。最も効果的な。)
したがって、実行できる唯一のことは、データをスライスしてすべてのコアをビジー状態に保つことです。
データをスライスする方法は3つあります。Aのサブレンジ、Bのサブレンジ、Nのサブレンジです。後者は、すでに独自の回答で行っています。
また、AまたはBの構造は、明らかにコレクション、または少なくともジェネレーターであることを除いて、明らかにしていません。
それらがメモリ(配列、リストなど)によって明示されたコレクションである場合、それらがかなりのサイズである場合、コアが何らかの形で協調して同時に実行されない限り、各コアでAとBを反復するとキャッシュが破壊される可能性がありますAとBの範囲を同時に設定すると、実際には互いにブーストされます。
しかし、それらがたまたま互いに大幅に同期しなくなった場合(たとえば、f
の条件付きロジックが同じものを評価していないため)、キャッシュをめぐって互いに争うことになります。
(例えは、ハードドライブ上の大きなフォルダ/ディレクトリのコピーを行うことです。異なるフォルダの別のコピーを同時に開始すると、両方のコピーがクロールに遅くなる可能性が高く、一緒にシリアル実行の合計の10倍を取ります。コピー。)
したがって、キャッシュの競合を軽減するために、それが実際に問題である場合は、個々のCPUの実行を、キャッシュに収まるA x Bマトリックスの一部に制限し、その制限されたセットですべてのCPUを動作させることを選択できます。すべてのCPUが完了するまでキャッシュに収まり、次にA x Bマトリックスの別のサブセットに移動します。 1つのCPUが最初に終了する場合、(a)このキャッシュのスラッシングはドメインの実際の問題であり、(b)すべてのCPUがA x Bサブセットでさらに終了すると仮定して、新しい作業を行うことすらしません。 or-lessと同時に。すべてのことを想定すると、次のサブセットに着手する前に、サブセットですべてのCPUを実行して完了する方がよいでしょう。
もちろん、オーバーヘッドも表すため、できる限り少ない数のスレッドを生成することも必要です。これは、回答をスライスする利点です。ただし、同期していないスレッドがキャッシュを十分に修正して、追加のスレッドを生成する価値がある可能性があります。
一方、コアとまったく同じ数のスレッドを生成しますが、A x Bのマトリックスサブレンジで明確に定義されたアルゴリズムで段階的に機能し、他のすべてのコアが完了を確認するのを待ってから、次のサブレンジで開始する場合があります。すべてのソリューションの中で最高。各スレッドはサブセットの完了をアナウンスし、すべてのスレッドが完了したという通知を待って自分自身を一時停止します。
したがって、各コアは、n/C反復の概念を使用して、同じA x Bサブセットを通過し、すべてのコアは次のA xBサブセットに移動します。
実際、シングルスレッドアルゴリズムでA x Bのサブ範囲を使用しても、A x B全体に何度も範囲を広げることでパフォーマンスが向上する可能性があります。これは、A xBマトリックスでタッチされたメモリがキャッシュに収まらない場合に完全に可能です。 。 A x Bを介して実行するたびに、両方のデータ構造全体をキャッシュに再度取り込む必要があります(A x Bを介して1回の反復で何度も何度も繰り返す必要があります)が、管理可能なサブセットで実行されている単一のスレッドは、そのような各サブセットを取り込むことができます。 n回の反復で1回だけキャッシュに挿入されるため、マトリックスサブセットのシングルスレッドバージョンから始めて、並列同期を追加することができます。
ループをクリーンに並列化するというはるかに一般的な問題の解決策は次のとおりです。
各スレッドにアイテム(この場合はa、bのペア)を1つずつプルさせ、すべてが消費されるまで部分的な結果を計算します。
メインスレッド:
output = 0
iter = iterator_over(A,B)
// start threads and wait until done
answer = output / size(A) / size(B)
return answer
各スレッド:
res = 0
while true:
synchronized:
if !iter.hasNext():
break
a,b = it.next()
res += f(a, b, n)
synchronized:
output += res
最適なパフォーマンスを得るには、スレッドの数をCPUコアの数と同じにする必要があります。
Cに多くのCPUコアがあり、これをCコアで並列実行したいとします。これが私の現在の解決策です:
最初のスレッド:
THREAD_1
count = 0
output = 0
for a in A:
for b in B:
output += f(a,b,n/C)
count += 1
thread_1_answer = output / count
return thread_1_answer
2番目のスレッド:
THREAD_2
count = 0
output = 0
for a in A:
for b in B:
output += f(a,b,n/C)
count += 1
thread_2_answer = output / count
return thread_2_answer
.。
C番目のスレッド:
THREAD_C
count = 0
output = 0
for a in A:
for b in B:
output += f(a,b,n/C)
count += 1
thread_C_answer = output / count
return thread_C_answer
最後に、最終的なanswerを次のように定義します。
answer = (thread_1_answer + thread_2_answer + ... + thread_C_answer)/C