2012-01-20 6 views
18

はスレッドサブクラスの次の定義(全体の実行可能なJavaソース・ファイルは、あなたの便宜のため、質問の最後に含まれている)を守って説明:このプログラムは、-Dparのスレッドを開始し、各スレッドのsz-Dsize/-Dparに設定します。プログラム実行時には、コマンドラインで-Dsizeとが設定されます。各スレッドオブジェクトには、新しい1024要素配列で初期化されたフィールドarrayがあります。その理由は、異なる数のスレッド間で同じ量の作業を分割したいからです。プログラムの規模を拡大することを期待しています。配列の割り当てと、Java仮想マシンとメモリの競合にアクセス

各スレッドが開始され、すべてのスレッドが完了するのに必要な時間が測定されます。以下に示すように、JIT関連の影響に対抗するために複数の測定を行います。各スレッドはループを行います。ループ内では、スレッドは配列内の512の位置の要素を偶数の反復で読み込み、奇数の反復で同じ要素を512に書き込みます。局所変数のみが変更されます。

完全なプログラムは以下の通りです。

分析-verbose:gcでテスト

- このプログラムの実行中に発生するいかなるガベージコレクションはありません。

実行]コマンド:

java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7 

CASE 1:そのためには、1,2,4,8スレッドの時間を実行している(7回の繰り返し):

>>> All running times: [2149, 2227, 1974, 1948, 1803, 2283, 1878] 
>>> All running times: [1140, 1124, 2022, 1141, 2028, 2004, 2136] 
>>> All running times: [867, 1022, 1457, 1342, 1436, 966, 1531] 
>>> All running times: [915, 864, 1245, 1243, 948, 790, 1007] 

私の考えは、非線形スケーリングは、メモリの競合によるものであるということでした。ちなみに、初期の反復は実際にはうまくいく - これは、異なる反復で配列が異なるメモリ領域に割り当てられるという事実のためかもしれない。

ケース2:次に、私はスレッドのrun方法でFoo[] arr = array行をコメントとrun方法自体に新しい配列を割り当てる:Foo[] arr = new Foo[1024]。測定:

>>> All running times: [2053, 1966, 2089, 1937, 2046, 1909, 2011] 
>>> All running times: [1048, 1178, 1100, 1194, 1367, 1271, 1207] 
>>> All running times: [578, 508, 589, 571, 617, 643, 645] 
>>> All running times: [330, 299, 300, 322, 331, 324, 575] 

今回は、すべてが予想どおりにスケールされます。私は配列が割り当てられた場所が何らかの役割を果たしているとは想像もしませんでしたが、明らかに何とかしています。私の考えは、以前は配列が互いに近くに配置されていて、メモリの競合が起き始めるということでした。

CASE 3:この仮定を検証するために、私は再びラインFoo[] arr = arrayコメントをはずしてきましたが、今回はメモリ内の位置が十分に互いに離れているように書かれていることを確認するためにnew Foo[32000]arrayフィールドを初期化します。ここでは、スレッドオブジェクトの作成中に割り当てられた配列を再度使用していますが、CASE1との違いは配列が大きいことだけです。

したがって、メモリの競合が原因であるようです。

プラットフォーム情報:

Ubuntu Server 10.04.3 LTS 
8 core Intel(R) Xeon(R) CPU X5355 @2.66GHz 
~20GB ram 
java version "1.6.0_26" 
Java(TM) SE Runtime Environment (build 1.6.0_26-b03) 
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode) 

質問:これは明らかに、メモリ競合の問題です。しかし、なぜこれは起こっているのですか?

  1. エスケープ解析は開始されていますか?その場合、CASE2のrunメソッドで作成された配列全体がスタックに割り当てられていることを意味しますか?このランタイム最適化の正確な条件は何ですか?確かに、配列は100万要素のスタックに割り当てられていませんか?

  2. アレイが ヒープ上に割り当てられているのとは逆にスタックに割り当てられていても、CASE1の場合でも、異なるスレッドによる2つの配列アクセスを少なくとも512 * 4バイト= 2kbで分割する必要があります。 !これは、どんなL1キャッシュラインよりも明らかに大きいです。これらの影響が誤った共有によるものである場合、完全に独立したいくつかのキャッシュラインへの書き込みがパフォーマンスにどのように影響しますか? (ここでは、各配列がJVM上の連続したメモリブロックを占めていることを前提としていますが、配列の作成時に割り当てられます。インテルXeonはccNUMAアーキテクチャを持っているので、代わりにL1キャッシュを使用してください - 私が間違っていれば私を訂正してください)

  3. 個々のスレッドが独自の新しいオブジェクトを独自に割り当てる場合は、配列がスレッドに割り当てられているときの競合がより少なくなる原因は何ですか?もしそうなら、参照が共有されると、ヒープガベージのその領域はどのように集められますか?

  4. アレイサイズを〜32000個に増加させた理由は、スケーラビリティ(メモリ競合の減少)ですか?これはメモリ階層内の正確な原因ですか?

参考にしてクレームをサポートしてください。

ありがとうございました!


全体の実行可能なJavaプログラム:

import java.util.ArrayList; 

class MultiStackJavaExperiment { 

    final class Foo { 
     int x = 0; 
    } 

    final class Worker extends Thread { 
     Foo[] array = new Foo[1024]; 
     int sz; 

     public Worker(int _sz) { 
      sz = _sz; 
     } 

     public void run() { 
      Foo[] arr = new Foo[1024]; 
      //Foo[] arr = array; 
      loop(arr); 
     } 

     public void loop(Foo[] arr) { 
      int i = 0; 
      int pos = 512; 
      Foo v = new Foo(); 
      while (i < sz) { 
       if (i % 2 == 0) { 
        arr[pos] = v; 
        pos += 1; 
       } else { 
        pos -= 1; 
        v = arr[pos]; 
       } 
       i++; 
      } 
     } 
    } 

    public static void main(String[] args) { 
     (new MultiStackJavaExperiment()).mainMethod(args); 
    } 

    int size = Integer.parseInt(System.getProperty("size")); 
    int par = Integer.parseInt(System.getProperty("par")); 

    public void mainMethod(String[] args) { 
     int times = 0; 
     if (args.length == 0) times = 1; 
     else times = Integer.parseInt(args[0]); 
     ArrayList <Long> measurements = new ArrayList <Long>(); 

     for (int i = 0; i < times; i++) { 
      long start = System.currentTimeMillis(); 
      run(); 
      long end = System.currentTimeMillis(); 

      long time = (end - start); 
      System.out.println(i + ") Running time: " + time + " ms"); 
      measurements.add(time); 
     } 

     System.out.println(">>>"); 
     System.out.println(">>> All running times: " + measurements); 
     System.out.println(">>>"); 
    } 

    public void run() { 
     int sz = size/par; 
     ArrayList <Thread> threads = new ArrayList <Thread>(); 

     for (int i = 0; i < par; i++) { 
      threads.add(new Worker(sz)); 
      threads.get(i).start(); 
     } 
     for (int i = 0; i < par; i++) { 
      try { 
       threads.get(i).join(); 
      } catch (Exception e) {} 
     } 
    } 

} 
+0

数字を混乱させて、あなたが探している結果を得るのは簡単です。私の答えを見ていただきありがとうございます。 –

+0

答えをありがとう、なぜあなたはそれを削除しましたか? – axel22

+0

私はあなたの分析と質問を読んだことがありませんでした。私は持っていなければならないし、あなたの質問に正しく答えたとは思わない。 –

答えて

13

溶液を得

-XX:+UseCondCardMarkフラグを付けてJVMを実行します。これはJDK7でのみ使用できます。これは問題を解決します。

説明基本的に

、ほとんどの管理ヒープ環境では発生書き込むにメモリの領域をマークするカードの表を使用しています。このようなメモリ領域は、書き込みが発生するとダーティーとマークされます。この情報はガベージコレクションに必要です。ダーティでないメモリ領域の参照はスキャンする必要はありません。カードは連続したメモリブロックで、通常は512バイトです。カードテーブルは通常、各カードに1バイトあります。このバイトが設定されている場合、カードは汚れています。これは、64バイトのカードテーブルが64 * 512バイトのメモリをカバーすることを意味します。そして、通常、現在のキャッシュラインサイズは64バイトです。

したがって、オブジェクトフィールドへの書き込みが発生するたびに、カードテーブル内の対応するカードのバイトをダーティとして設定する必要があります。シングルスレッドプログラムでの便利な最適化は、単に関連するバイトをマークすることでこれを行うことです。毎回書き込みを行います。最初にバイトがセットされ、条件付き書込みが追加の読取りと条件付きジャンプを必要とするかどうかを確認する代わりに、少し遅くなります。

しかし、この最適化は、隣接するカードがカードテーブル内の隣接するバイトへの書き込みを要求するように書き込まれているので、メモリに複数のプロセッサが書き込まれている場合に致命的となる可能性があります。したがって、書き込まれるメモリ領域(上記の配列のエントリ)は、メモリ競合の通常の原因である同じキャッシュラインにありません。本当の理由は、書き込まれる汚れたバイトが同じキャッシュラインにあるということです。

上記のフラグは、最初にバイトが設定されているかどうかを確認し、そうでない場合にのみ設定することによって、カードテーブルのダーティバイト書き込みを実装します。このようにして、メモリ競合は、そのカードへの最初の書き込み中にのみ起こります。その後、そのキャッシュラインへの読み取りだけが発生します。キャッシュラインは読み出されるだけなので、複数のプロセッサに複製することができ、読み込みのために同期する必要はありません。

私はこのフラグが1スレッドの場合、実行時間を約15〜20%増加させることがわかりました。

このblog postおよびbug reportには、-XX:+UseCondCardMarkフラグが説明されています。

関連する並行性メーリングリストのディスカッション:Array allocation and access on the JVM

1

私はその混乱の問題かもしれない偶発的なものをたくさんやっていないので、あなたのコードを削減する必要があると考えています。コードを減らした後、毎回同じ配列の場所にしかアクセスしていないことは私には明らかです。つまり位置512です。

コードを最小限に抑える場合は、スレッドを再開して停止/開始しないと、再現性の高い結果が得られます。

import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 
import java.util.concurrent.ExecutionException; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Future; 

public class MultiStackJavaExperiment { 
    static final int size = Integer.getInteger("size", 500000000); 

    public static void main(String... args) throws ExecutionException, InterruptedException { 
     int par = 8; 
     for (int s = 64; s <= 64 * 1024; s *= 2) { 
      int times = args.length == 0 ? 1 : Integer.parseInt(args[0]); 
      long[] measurements = new long[times]; 

      ExecutorService es = Executors.newFixedThreadPool(par); 
      List<Future<?>> futures = new ArrayList<Future<?>>(times); 
      for (int i = 0; i < times; i++) { 
       long start = System.currentTimeMillis(); 
       final int sz = size/par; 
       futures.clear(); 
       for (int j = 0; j < par; j++) { 
        final Object[] arr = new Object[s]; 
        futures.add(es.submit(new Runnable() { 
         @Override 
         public void run() { 
          final int bits = 7, arraySize = 1 << bits; 
          int i = 0; 
          int pos = 32; 
          Object v = new Object(); 
          while (i < sz) { 
           if (i % 2 == 0) { 
            arr[pos] = v; 
            pos += 1; 
           } else { 
            pos -= 1; 
            v = arr[pos]; 
           } 
           i++; 
          } 
         } 
        })); 
       } 
       for (Future<?> future : futures) 
        future.get(); 

       long time = System.currentTimeMillis() - start; 
//    System.out.println(i + ") Running time: " + time + " ms"); 
       measurements[i] = time; 
      } 
      es.shutdown(); 
      System.out.println("par = " + par + " arr.length= "+ s + " >>> All running times: " + Arrays.toString(measurements)); 
     } 
    } 
} 

これはアクセス値の間の距離が重要であることを示します。配列を割り当てることは、各スレッドであることで、あなたはあなたがtlabオフ

par = 8 arr.length= 64 >>> All running times: [225, 151, 151, 150, 152, 153, 152] 
par = 8 arr.length= 256 >>> All running times: [155, 151, 151, 151, 151, 151, 155] 
par = 8 arr.length= 1024 >>> All running times: [153, 152, 151, 151, 151, 155, 152] 
par = 8 arr.length= 4096 >>> All running times: [155, 156, 151, 152, 151, 155, 155] 
par = 8 arr.length= 16384 >>> All running times: [154, 157, 152, 152, 158, 153, 153] 
par = 8 arr.length= 65536 >>> All running times: [155, 157, 152, 184, 181, 154, 153] 
par = 8 arr.length= 262144 >>> All running times: [240, 159, 166, 151, 172, 154, 160] 
par = 8 arr.length= 1048576 >>> All running times: [165, 162, 163, 162, 163, 162, 163] 

電源を入れ付きを得るスレッド内のアレイを移動する場合(スペースアウトブロック内のデータ)

par = 8 arr.length= 64 >>> All running times: [539, 413, 444, 444, 457, 444, 456] 
par = 8 arr.length= 256 >>> All running times: [398, 527, 514, 529, 445, 441, 445] 
par = 8 arr.length= 1024 >>> All running times: [419, 507, 477, 422, 412, 452, 396] 
par = 8 arr.length= 4096 >>> All running times: [316, 282, 250, 232, 242, 229, 238] 
par = 8 arr.length= 16384 >>> All running times: [316, 207, 209, 212, 208, 208, 208] 
par = 8 arr.length= 65536 >>> All running times: [211, 211, 208, 208, 208, 291, 206] 
par = 8 arr.length= 262144 >>> All running times: [366, 210, 210, 210, 210, 209, 211] 
par = 8 arr.length= 1048576 >>> All running times: [296, 211, 215, 216, 213, 211, 211] 

を異なるTLABsを使用します-XX:-UseTLABと同じコードがsyouに

par = 8 arr.length= 64 >>> All running times: [608, 467, 467, 457, 468, 461, 428] 
par = 8 arr.length= 256 >>> All running times: [437, 437, 522, 512, 522, 369, 535] 
par = 8 arr.length= 1024 >>> All running times: [394, 395, 475, 525, 470, 440, 478] 
par = 8 arr.length= 4096 >>> All running times: [347, 215, 238, 226, 236, 204, 271] 
par = 8 arr.length= 16384 >>> All running times: [291, 157, 178, 151, 150, 151, 152] 
par = 8 arr.length= 65536 >>> All running times: [163, 152, 162, 151, 159, 159, 154] 
par = 8 arr.length= 262144 >>> All running times: [164, 172, 152, 169, 160, 161, 160] 
par = 8 arr.length= 1048576 >>> All running times: [295, 153, 164, 153, 166, 154, 163] 
+0

ありがとうございます。 1)私は同じ位置にアクセスしていますが、異なる配列にあります。 2)毎回再現可能な結果が得られています。 3)スレッドオブジェクトはガベージコレクションされていません - これをチェックしました。そのため、起動と停止はパフォーマンスに影響しないはずです(120msよりもはるかに短くなります)。 4) 'Object [] arr = new Object [1024];'という行を作成している 'Runnable'匿名クラスに移動して、クラスのフィールドにしてください。私はそれが拡大しないと期待しています。 – axel22

+0

@アクセル22内にあります。その外側に移動したとき(つまり配列が共有されているとき)の結果を追加しました。これはうまくスケールされません。つまり、複数のスレッドから同じデータへの書き込みを避けるためにコードを変更します。それが不可能な場合は、最速の解決策は1つのスレッドを使用することです。 –

+0

配列は以下のように割り当てられます:1) 'Runnable'の外側で - それは共有され、2)' Runnable'はフィールドとして、** Runnableには独自の配列があります**共有されません。 3) 'run'メソッドの中に - それも**共有されていない**。あなたのコードによれば、現在 'run'メソッドの内部に割り当てられています。私の質問は「2)」と「3)」のパフォーマンスの違いに関するものでした。私が正しく理解していれば、あなたは '1)'と '3')を比較しました。 – axel22

関連する問題