5

Nehalem プロセッサで実行されるマルチスレッド Java アプリケーションを作成しています。ただし、4 つのスレッドから開始すると、アプリケーションのスピードアップがほとんど見られないという問題があります。

私はいくつかの簡単なテストを行いました。大きな配列を割り当て、配列内のランダムなエントリにアクセスするだけのスレッドを作成しました。そのため、スレッド数を実行しても、実行時間は変化しません (使用可能な CPU コアの数を超えていないと仮定します)。しかし、私が観察したところ、1 つまたは 2 つのスレッドを実行するとほぼ同じ時間がかかりますが、4 つまたは 8 つのスレッドを実行すると大幅に遅くなります。したがって、アプリケーションでアルゴリズムと同期の問題を解決しようとする前に、達成できる最大の可能な並列化を見つけたいと思います。

JVM オプションを使用-XX:+UseNUMAしたので、対応するスレッドの近くのメモリに配列を割り当てる必要があります。

PS スレッドが単純な数学的計算を行っている場合、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) {
            };
    }
}

ご覧のとおり、このミニテストでは同期がまったく行われず、配列の割り当てもスレッド内にあるため、すばやくアクセスできるメモリのチャンクに配置する必要があります。また、このコードにはメモリ競合はありません。それでも 4 スレッドの場合、実行時間は 30% 低下し、8 スレッドでは実行速度が 2 倍遅くなります。コードからのように、すべてのスレッドが作業を完了するまで待ちます。スレッドの作業は独立しているため、スレッドの数は実行にかかる合計時間に影響しません。

マシンには 2 つのクアッドコア ハイパースレッド Nehalem プロセッサ (合計 16 個の CPU) がインストールされているため、8 つのスレッドでそれぞれがその CPU を排他的にキャッチできます。

より小さな配列 (20K エントリ) でこのテストを実行しようとしたとき、4 スレッドの実行時間の低下は 7% で、8 スレッドでは 14% であり、満足のいくものでした。しかし、大きな配列(40M エントリ)でランダム アクセスを実行しようとすると実行時間が劇的に増加するため、メモリの大きなチャンク(キャッシュ メモリに収まらないため?)が非アクセスでアクセスされるという問題があると思います。 -効率的な方法。

これを修正する方法はありますか?

これにより、質問がより明確になることを願っています。ありがとうございます。

4

6 に答える 6

3

テストのボトルネックは、CPU からメモリへの帯域幅です。ローカル メモリが使用可能な場合でも、いくつかのスレッドで共有されます。(メモリはノードに対してローカルであり、特定のコアに対してではありません。)CPU が上記のテストのような単純なループで使用可能な帯域幅を簡単に超えることができるため、そのようなテストでスレッドを増やしてもパフォーマンスは向上せず、パフォーマンスが低下する可能性があります。キャッシュの一貫性が低下したためです。

健全性テストだけですが、パラレルコレクターも使用していますか? -XX:+UseParallelGC. UseNUMA が有効になるのはそのときだけです。

于 2010-07-15T13:35:16.950 に答える
1

正確に何をしているのか、解決しようとしている問題は何かを知らずに。十分にスケーラブルでない主な理由である可能性があるため、コードの周りで同期が頻繁に行われているようです。同期が過剰になると、アプリケーションがほぼシリアルになると、スピードアップが遅くなります。したがって、私の提案は、実装を調べて、これを理解しようとすることです。

追加。

あなたがしていることの実装を追加した後。パフォーマンスの低下は、大量の大量のメモリ アクセスによって説明できます。すべてのスレッドを実行すると、キャッシュされていないデータのためにメモリ コントローラーにアクセスする必要があります。それらは異なる CPU で実行されているため、メモリ コントローラーは CPU が同時にそれを行うことを防ぎます。つまり、キャッシュ ミスごとにハードウェア レベルで同期が行われます。あなたの場合、10個の異なる独立したプログラムを実行しているかのようにほぼ同じです。たとえば、Web ブラウザーを 10 コピー (10 を任意の数に置き換えることができます) 起動すると、同じ効果が得られると思いますが、これはブラウザーの実装が効果的でないという意味ではありません。コンピュータのメモリ。

于 2010-07-15T06:37:14.633 に答える
0

投稿した記事からのアドバイスでテストを修正しました。私の 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
于 2010-07-15T13:19:27.903 に答える
0

頭に浮かぶ明らかな潜在的な問題が 2 つあります。

  • より多くのスレッドを使用すると、より多くの配列が割り当てられ、キャッシュがバーストします。メイン メモリまたは下位レベルのキャッシュへのアクセスは、はるかに遅くなります。
  • 乱数ジェネレーターのインスタンスと同じソースを使用している場合、スレッドはそれへのアクセスをめぐって争うことになります。完全な同期ではなく、ロックフリー アルゴリズムによるメモリ バリアです。一般に、ロックフリー アルゴリズムは高速ですが、競合が多い場合は非常に遅くなります。
于 2010-07-15T08:18:57.140 に答える
0

並行性の問題を除けば、スローアップの原因として最も可能性が高いのは、メモリ キャッシュの競合です。

すべてのスレッドが同じストレージにアクセスしている場合、他のプロセッサのメモリ キャッシュにアクセスする可能性があります。

ストレージが「読み取り専用」の場合、各スレッドに独自のコピーを与えることができます。これにより、JVM とプロセッサがメモリ アクセスを最適化できるようになります。

于 2010-07-15T08:25:28.310 に答える
0

Artem が指摘しているように、不要な同期が発生する可能性があります。しかし、私は事実を確立することから始めます。あなたが説明したように、あなたのアプリは本当に遅く動作していますか?

この件に関する洞察に満ちた記事は次のとおりです

特に並行コードを扱っている場合、有用なマイクロ ベンチマークを作成するのは実際には非常に困難です。たとえば、実行されていると思われるコードをコンパイラが最適化する「デッド コードの削除」を行うことができます。ガベージ コレクションがいつ実行されるかを推測することも困難です。Hotspot のランタイム最適化により、測定もより困難になります。スレッドの場合は、スレッドの作成にかかる時間を考慮する必要があります。したがって、正確な測定を行うには `CyclicBarrier` などを使用する必要があるかもしれません。そういうもの..

そうは言っても、読んでいるだけだと、メモリへのアクセスに問題が発生するのは難しいと思います。コードを投稿していただければ、より適切にサポートできる可能性があります...

于 2010-07-15T07:11:18.490 に答える