複数のスレッドによるメモリアクセス
-
16-09-2020 - |
質問
Nehalemプロセッサで動作するマルチスレッドJavaアプリケーションを書きます。ただし、4つのスレッドから起動すると、アプリケーションのスピードアップがほとんど表示されないという問題があります。
私は簡単なテストをしました。私は大きな配列を割り当て、アレイ内のランダムエントリにアクセスするスレッドを作成しました。したがって、スレッド数を実行すると、実行時は変更されないはずです(利用可能なCPUコア数を超えていないと仮定)。しかし、私が観察したことは、1つか2つのスレッドを実行することであることですが、4つか8つのスレッドが著しく遅くなっています。だから私のアプリケーションでアルゴリズムと同期の問題を解決する前に、私は達成できる最大限の並列化が何であるかを見つけたいです。
-XX:+UseNUMA
JVMオプションを使用しているため、配列は対応するスレッド近くのメモリに割り当てられるべきです。
p.S。スレッドが簡単な数学計算をしていた場合、4つのスレッドと8つのスレッドの時間がかかりませんでしたので、スレッドがメモリにアクセスしているときにいくつかの問題があります。
あらゆる助言やアイデアが高く評価されています。
編集
返事のためにあなたのすべてありがとう。私は自分自身を十分によく説明していないことがわかります。
私のアプリケーションで同期の問題を解消しようとする前に、達成できる最良の並列化をチェックする簡単なテストを作成しました。コードは次のとおりです。
public class TestMultiThreadingArrayAccess {
private final static int arrSize = 40000000;
private class SimpleLoop extends Thread {
public void run() {
int array[] = new int[arrSize];
for (long i = 0; i < arrSize * 10; i++) {
array[(int) ((i * i) % arrSize)]++; // randomize a bit the access to the array
}
long sum = 0;
for (int i = 0; i < arrSize; i++)
sum += array[i];
}
}
public static void main(String[] args) {
TestMultiThreadingArrayAccess test = new TestMultiThreadingArrayAccess();
for (int threadsNumber : new int[] { 1, 2, 4, 8 }) {
Statistics timer = new Statistics("Executing " + threadsNumber+ " threads"); // Statistics is a simple helper class that measures the times
timer.start();
test.doTest(threadsNumber);
timer.stop();
System.out.println(timer.toString());
}
}
public void doTest(int threadsNumber) {
Thread threads[] = new Thread[threadsNumber];
for (int i = 0; i < threads.length; i++) {
threads[i] = new SimpleLoop();
threads[i].start();
}
for (int i = 0; i < threads.length; i++)
try {
threads[i].join();
} catch (InterruptedException e) {
};
}
}
.
では、このMINITEST全体に同期がないため、アレイの割り当てもスレッド内にあるため、すばやくアクセスできるメモリのチャンクに配置する必要があります。このコードにはメモリの満足はありません。それでも4スレッドの場合、ランニングタイムには30%のドロップがあり、8つのスレッドが2回遅くなります。コードからのように、すべてのスレッドが自分の仕事を終えるまで待ってください。また、自分の作業は独立したスレッド数が実行される合計時間に影響を与えるべきではありません。
マシン上に2つのクアッドコアのハイパースレッドNehalemプロセッサ(完全に16のCPU)を取り付けたので、8つのスレッドでそれぞれがCPUを排他的にキャッチすることができます。
このテストを小さい配列(20kエントリ)でこのテストを実行しようとしたとき、4スレッドの実行時間のドロップは7%と8個のスレッド - 14%でした。しかし、私がラージアレイ(40mのエントリ)でアクセスされたランダムを操作しようとすると、実行時間が劇的に増加するので、(キャッシュメモリに収まるため、キャッシュメモリに収まらないため)という問題があると考えています。効率的な方法
これを修正する方法はありますか?
これがより良い方法で質問をまとめていることを願っています、またありがとうございました。
解決
テストのボトルネックは、メモリ帯域へのCPUです。ローカルメモリが利用可能な場合でも、あるスレッド数によって共有される予定です。(メモリは、特定のコアではなく、ノードにローカルです。)CPUが上記のテストのような単純なループのために使用可能な帯域幅を簡単に超えると、そのようなテストでのスレッドが増えているため、パフォーマンスが向上し、パフォーマンスが悪くなります。悪化したキャッシュコヒーレンスによる。
ほとんどテストだけ、あなたはパラレルコレクターを使用していますか?-XX:+UseParallelGC
。usenumaはそれからだけ有効になります。
他のヒント
あなたが何をしているのか知らず、解決しようとしている問題は何ですか。それはあなたが十分にスケーラブルにならないことの主な理由かもしれませんので、あなたはあなたのコードの周りに重い同期があるように見えます。同期により、アプリケーションがほぼシリアルになったら、スピードアップを遅くします。だからあなたへの私の提案はあなたの実装を検査し、これを理解しようとしています。
追加
あなたがしていることの実装を追加した後。性能の低下は、大きくて大規模なメモリアクセスによって説明することができます。スレッドをすべて実行してキャッシュされていないデータのためにメモリコントローラにアクセスする必要があると、メモリコントローラはCPUが同時に実行されるのを防ぎます。つまり、各キャッシュミスでハードウェアレベルで同期があります。あなたの場合は、10の異なる独立したプログラムを実行していたかのようにそれはほとんど同じです。たとえば、Webブラウザをコピーする(たとえば、10個置換することができます)。たとえば、Webブラウザをコピーすることができますが、これはブラウザの実装が無効であることを意味しません。コンピュータメモリ
AREREMノートとして、不要な同期が可能です。しかし、私は事実を確立することから始めました。あなたが説明するようにあなたのアプリは本当に遅く走りますか?
これは主題に関する洞察的な記事です:
特にあなたが同時コードを扱っているとき、便利なマイクロベンチマークを書くのは実際にはかなり難しいです。たとえば、コンパイラが実行されていると思われるコードを最適化する「デッドコードの消去」を持つことができます。ガベージコレクションが実行されたときに推測するのも難しいです。ホットスポットのランタイム最適化もまた測定をより困難にする。スレッドの場合は、それらを作成するために使用される時間を考慮に入れる必要があります。だからあなたは正確な測定をするために `circiCarrier`などを使う必要があるかもしれません。そのようなもの。
それを言ったことは、あなたがしていることが読んでいるのであれば、あなたがメモリへのアクセスに問題があるでしょう。コードを投稿できる場合は、私たちはあなたを助けることができるかもしれません...
湧き出る2つの明白な潜在的な問題があります。
- より多くのスレッドを使用すると、キャッシュを破棄するアレイが多い。メインメモリまたは低レベルのキャッシュへのアクセスははるかに遅いです。
- あなたが乱数ジェネレータの同じインスタンスのソースを使用しているならば、スレッドはそれへのアクセスを介して戦っているでしょう。完全な同期ではありませんが、代わりにロックフリーアルゴリズムを持つメモリバリアです。一般的にロックフリーのアルゴリズムは、一般的に速いですが、高い競合の下ではるかに遅くなります。
並行性の問題からの推奨されているあなたのスローアップの最も可能性の高い原因はメモリキャッシュの競合です。
すべてのスレッドが同じストレージにアクセスしている場合、アクセスしたいときに他のプロセスメモリキャッシュにチャンスがあります。
記憶域が「読み取り専用」の場合、各スレッドにJVM&Processorがメモリアクセシブを最適化できるようにする独自のコピーを提供できます。
投稿した記事からのアドバイスでテストを修正しました。私の2つのコアマシンで(今私が今持っているのはそれだけです)結果は合理的なようです(私は各スレッド番号について2テストを実行しました):
多分これを試すことができますか? (私はあなたのテストをわずかに修正しなければならなかったことに注意してください(コメントを参照)。
-server
オプションを使用してこのテストを実行することもできます。
Test with threadNum 1 took 2095717473 ns
Test with threadNum 1 took 2121744523 ns
Test with threadNum 2 took 2489853040 ns
Test with threadNum 2 took 2465152974 ns
Test with threadNum 4 took 5044335803 ns
Test with threadNum 4 took 5041235688 ns
Test with threadNum 8 took 10279012556 ns
Test with threadNum 8 took 10347970483 ns
.
コード:
import java.util.concurrent.*;
public class Test{
private final static int arrSize = 20000000;
public static void main(String[] args) throws Exception {
int[] nums = {1,1,2,2,4,4,8,8};//allow hotspot optimization
for (int threadNum : nums) {
final CyclicBarrier gate = new CyclicBarrier(threadNum+1);
final CountDownLatch latch = new CountDownLatch(threadNum);
ExecutorService exec = Executors.newFixedThreadPool(threadNum);
for(int i=0; i<threadNum; i++){
Runnable test =
new Runnable(){
public void run() {
try{
gate.await();
}catch(Exception e){
throw new RuntimeException(e);
}
int array[] = new int[arrSize];
//arrSize * 10 took very long to run so made it
// just arrSize.
for (long i = 0; i < arrSize; i++) {
array[(int) ((i * i) % arrSize)]++;
}//for
long sum = 0;
for (int i = 0; i < arrSize; i++){
sum += array[i];
}
if(new Object().hashCode()==sum){
System.out.println("oh");
}//if
latch.countDown();
}//run
};//test
exec.execute(test);
}//for
gate.await();
long start = System.nanoTime();
latch.await();
long finish = System.nanoTime();
System.out.println("Test with threadNum " +
threadNum +" took " + (finish-start) + " ns ");
exec.shutdown();
exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);
}//for
}//main
}//Test
.