2016-10-07 8 views
9

をソートしながら、私は現在、私たちがしなければならないものの一つは、一般的な種類のいくつかを記述している、あなたが期待することとして、データ構造のクラスを取っています。私の挿入ソートアルゴリズムを書いているうちに、インストラクターのものよりも大幅に速く走っていたことがわかりました(400000データポイントの場合、アルゴリズムは約30秒、約90秒でした)。私は彼に私のコードを電子メールで送り、両方が同じマシン上で走っていたときに同じ結果が起こった。私たちは、ソート方法をゆっくりと私のものに変えるのに40分以上を費やすことに成功しました。まず、ここに私の挿入ソートのコードは次のとおりです。非常に奇妙な効率性に関する注意点

public static int[] insertionSort(int[] A){ 

    //Check for illegal cases 
    if (A == null || A.length == 0){ 

     throw new IllegalArgumentException("A is not populated"); 

    } 

    for(int i = 0; i < A.length; i++){ 

     int j = i; 

     while(j > 0 && A[j - 1] > A[j]){ 

      int temp = A[j]; 
      A[j] = A[j - 1]; 
      A[j - 1] = temp; 

      j--; 

     } 

    } 

    return A; 

} 

は今、この時点で彼のコードは、私たちがA[j]A[j - 1]をスワップラインを除いて地雷と全く同じでした。彼のコードは次のようになりました:

int temp = A[j - 1]; 
A[j - 1] = A[j]; 
A[j] = temp; 

これらの3行が犯人であることがわかりました。私のコードはこれによりかなり速く実行されていました。困惑し、私たちは、配列の宣言、int jための変数宣言と私はそれらを書いたように、彼がそれを書いたようにスワップのためのコードの3行が含まれていmainを持っていた簡単なプログラムのバイトコードを取得するにはjavap -cを走りました。ここに私のスワップ方式のバイトコードは次のとおりです。

Compiled from "me.java" 
public class me { 
    public me(); 
    Code: 
     0: aload_0 
     1: invokespecial #1     // Method java/lang/Object."<init>":()V 
     4: return 

    public static void main(java.lang.String[]); 
    Code: 
     0: sipush  10000 
     3: newarray  int 
     5: astore_1 
     6: bipush  10 
     8: istore_2 
     9: aload_1 
     10: iload_2 
     11: iaload 
     12: istore_3 
     13: aload_1 
     14: iload_2 
     15: aload_1 
     16: iload_2 
     17: iconst_1 
     18: isub 
     19: iaload 
     20: iastore 
     21: aload_1 
     22: iload_2 
     23: iconst_1 
     24: isub 
     25: iload_3 
     26: iastore 
     27: return 
} 

そして、私の講師のメソッドのバイトコード:

Compiled from "instructor.java" 
public class instructor { 
    public instructor(); 
    Code: 
     0: aload_0 
     1: invokespecial #1     // Method java/lang/Object."<init>":()V 
     4: return 

    public static void main(java.lang.String[]); 
    Code: 
     0: sipush  10000 
     3: newarray  int 
     5: astore_1 
     6: bipush  10 
     8: istore_2 
     9: aload_1 
     10: iload_2 
     11: iconst_1 
     12: isub 
     13: iaload 
     14: istore_3 
     15: aload_1 
     16: iload_2 
     17: iconst_1 
     18: isub 
     19: aload_1 
     20: iload_2 
     21: iaload 
     22: iastore 
     23: aload_1 
     24: iload_2 
     25: iload_3 
     26: iastore 
     27: return 
} 

私はこれらのバイトコードとの間の実質的な違いは表示されません。 この奇妙な振る舞いを引き起こす原因は何か(私のコードはまだ彼より3倍も速く走っていて、アルゴリズムのより大きな配列にこのような違いがあると予想されます)これは単にJavaの変わった変わったものですか?さらに、これはお使いのコンピュータで発生しますか?参考のため、をこれはMacBook Proの半ば2014上で実行され、それがここに表示され、それは、これらの3行を除いて、ここで表示される彼のコードは正確にコードに推定されたとおりに私のコードです。

public class Tester1 { 

    public static void main(String[] args){ 

     int[] A = new int[400000]; 

     for(int i = 0; i < A.length; i++){ 

      A[i] = (int) (Math.random() * Integer.MAX_VALUE); 

     } 

     double start = System.currentTimeMillis(); 
     insertionSort(A); 
     System.out.println("My insertion sort took " + (System.currentTimeMillis() - start) + " milliseconds."); 


    } 

    public static int[] insertionSort(int[] A){ 

     //Check for illegal cases 
     if (A == null || A.length == 0){ 

      throw new IllegalArgumentException("A is not populated"); 

     } 

     for(int i = 0; i < A.length; i++){ 

      int j = i; 

      while(j > 0 && A[j - 1] > A[j]){ 

       int temp = A[j]; 
       A[j] = A[j - 1]; 
       A[j - 1] = temp; 

       j--; 

      } 

     } 

     return A; 

    } 

} 

そして、第二のファイル:

public class Tester2 { 

    public static void main(String[] args){ 

     int[] A = new int[400000]; 

     for(int i = 0; i < A.length; i++){ 

      A[i] = (int) (Math.random() * Integer.MAX_VALUE); 

     } 

     double start = System.currentTimeMillis(); 
     otherInsertion(A); 
     System.out.println("Other insertion sort took " + (System.currentTimeMillis() - start) + " milliseconds."); 


    } 


    public static int[] otherInsertion(int[] A){ 

     //Check for illegal cases 
     if (A == null || A.length == 0){ 

      throw new IllegalArgumentException("A is not populated"); 

     } 

     for(int i = 0; i < A.length; i++){ 

      int j = i; 

      while(j > 0 && A[j - 1] > A[j]){ 

       int temp = A[j - 1]; 
       A[j - 1] = A[j]; 
       A[j] = temp; 

       j--; 

      } 

     } 

     return A; 

    } 

} 

して出力する(引数なしで、ちょうどjava Tester1java Tester2):ここで

[EDIT]は私のテストクラスです

My insertion sort took 37680.0 milliseconds. 
Other insertion sort took 86358.0 milliseconds. 

これらは実行されました2つの異なるJVMで2つの別々のファイルとして扱います。

+0

興味深い。あなたはあなたのテストクラスの1つを投稿できますか? –

+0

@JonKiparskyしました。また、私はそれが効率的である周りに交換しました。 A [j - 1]にtempを割り当てる方が効率的だったようです。また、一時的なスワップはそれをさらに悪化させます。 –

+0

奇数、対応するネイティブコードには何か明白ですか? – harold

答えて

6

これは共通 部分式の除去とともにアンローリング最適化ループの効果です。配列アクセス命令の順序に応じて、JITはあるケースでは冗長ロードを排除できますが、他のケースでは重複ロードを排除できます。

詳しく説明します。どちらの場合も、JITは内部ループの反復を4回展開します。

など。あなたのケースのために:

while (j > 3) { 
     if (A[j - 1] > A[j]) { 
      int temp = A[j]; 
      A[j] = A[j - 1]; 
      A[j - 1] = temp;   \ 
     }        A[j - 1] loaded immediately after store 
     if (A[j - 2] > A[j - 1]) { /
      int temp = A[j - 1]; 
      A[j - 1] = A[j - 2]; 
      A[j - 2] = temp;   \ 
     }        A[j - 2] loaded immediately after store 
     if (A[j - 3] > A[j - 2]) { /
      int temp = A[j - 2]; 
      A[j - 2] = A[j - 3]; 
      A[j - 3] = temp;   \ 
     }        A[j - 3] loaded immediately after store 
     if (A[j - 4] > A[j - 3]) { /
      int temp = A[j - 3]; 
      A[j - 3] = A[j - 4]; 
      A[j - 4] = temp; 
     } 
     j -= 4; 
    } 

はその後JITは、冗長アレイの負荷を排除し、そして得られたアセンブリが見えます

0x0000000002d53a70: movslq %r11d,%r10 
0x0000000002d53a73: lea 0x0(%rbp,%r10,4),%r10 
0x0000000002d53a78: mov 0x10(%r10),%ebx ; ebx = A[j] 
0x0000000002d53a7c: mov 0xc(%r10),%r9d  ; r9d = A[j - 1] 

0x0000000002d53a80: cmp %ebx,%r9d   ; if (r9d > ebx) { 
0x0000000002d53a83: jle 0x0000000002d539f3 
0x0000000002d53a89: mov %r9d,0x10(%r10) ;  A[j] = r9d 
0x0000000002d53a8d: mov %ebx,0xc(%r10)  ;  A[j - 1] = ebx 
               ; } 
0x0000000002d53a91: mov 0x8(%r10),%r9d  ; r9d = A[j - 2] 

0x0000000002d53a95: cmp %ebx,%r9d   ; if (r9d > ebx) { 
0x0000000002d53a98: jle 0x0000000002d539f3      
0x0000000002d53a9e: mov %r9d,0xc(%r10)  ;  A[j - 1] = r9d  
0x0000000002d53aa2: mov %ebx,0x8(%r10)  ;  A[j - 2] = ebx 
               ; }    
0x0000000002d53aa6: mov 0x4(%r10),%r9d  ; r9d = A[j - 3]  

0x0000000002d53aaa: cmp %ebx,%r9d   ; if (r9d > ebx) { 
0x0000000002d53aad: jle 0x0000000002d539f3      
0x0000000002d53ab3: mov %r9d,0x8(%r10)  ;  A[j - 2] = r9d 
0x0000000002d53ab7: mov %ebx,0x4(%r10)  ;  A[j - 3] = ebx 
               ; }     
0x0000000002d53abb: mov (%r10),%r8d  ; r8d = A[j - 4] 

0x0000000002d53abe: cmp %ebx,%r8d   ; if (r8d > ebx) { 
0x0000000002d53ac1: jle 0x0000000002d539f3 
0x0000000002d53ac7: mov %r8d,0x4(%r10)  ;  A[j - 3] = r8 
0x0000000002d53acb: mov %ebx,(%r10)  ;  A[j - 4] = ebx 
               ; } 
0x0000000002d53ace: add $0xfffffffc,%r11d ; j -= 4 
0x0000000002d53ad2: cmp $0x3,%r11d   ; while (j > 3) 
0x0000000002d53ad6: jg  0x0000000002d53a70 

ループ展開後に異なりますあなたのインストラクターのコードのように:

while (j > 3) { 
     if (A[j - 1] > A[j]) { 
      int temp = A[j - 1]; 
      A[j - 1] = A[j]; 
      A[j] = temp;   <-- another store instruction between A[j - 1] access 
     } 
     if (A[j - 2] > A[j - 1]) { 
      int temp = A[j - 2]; 
      A[j - 2] = A[j - 1]; 
      A[j - 1] = temp; 
     } 
     ... 

JVMますA[j - 1]の前のロードの後に​​別のストア命令があるので、A[j - 1]の後続のロードを削除しないでください(ただし、そのような最適化は理論的に可能である)。 、両方の例パフォーマンスを(-XX:LoopUnrollLimit=0)を使用すると、ループアンローリング最適化とJVMを実行する場合は無効になっていること、

0x0000000002b53a00: cmp %r8d,%r10d   ; if (r10d > r8d) { 
0x0000000002b53a03: jle 0x0000000002b53973 
0x0000000002b53a09: mov %r8d,0xc(%rbx)  ;  A[j - 1] = r8d 
0x0000000002b53a0d: mov %r10d,0x10(%rbx) ;  A[j] = r10d 
               ; } 
0x0000000002b53a11: mov 0xc(%rbx),%r10d  ; r10d = A[j - 1] 
0x0000000002b53a15: mov 0x8(%rbx),%r9d  ; r9d = A[j - 2] 

0x0000000002b53a19: cmp %r10d,%r9d   ; if (r9d > r10d) { 
0x0000000002b53a1c: jle 0x0000000002b53973 
0x0000000002b53a22: mov %r10d,0x8(%rbx)  ;  A[j - 2] = r10d 
0x0000000002b53a26: mov %r9d,0xc(%rbx)  ;  A[j - 1] = r9d  
               ; } 
0x0000000002b53a2a: mov 0x8(%rbx),%r8d  ; r8d = A[j - 2] 
0x0000000002b53a2e: mov 0x4(%rbx),%r10d  ; r10d = A[j - 3] 

注:

ので、アセンブリコードは、より多くのロード命令を持つことになり、パフォーマンスが悪くなります同じになります。

P.S.両方の方法の完全分解がhere利用可能である、あなたが説明するJavaバイトコードには何も表示されません
-XX:CompileOnly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

+0

。見知らぬ人でも何かが起こった。私は今日、同じテストコードを最初にパラメータなしで実行し、結果はスワップされました: '私の挿入の種類は56634.0ミリ秒でした。 その他の挿入の種類は146979.0ミリ秒でした。 また、 '-XX:LoopUnrollLimit = 0'で実行した場合、これは遅くなりました。 '私の挿入の種類は75795.0ミリ秒でした。 その他の挿入の種類は141911.0ミリ秒でした。 コードは変更されませんでした。私のコードはまだ 'int temp = A [j]'です。これらのテストには 'javac'コンパイラを使いました。 –

+0

@DylanSiegler 1つのメソッドで、または1つのJVMでも2つのアルゴリズムをベンチマークしないでください。これは予期しない結果につながる可能性があります。なぜこれが間違っているのかを記述した[良い記事](http://www.oracle.com/technetwork/articles/java/architect-benchmarking-2266277.html)があります。 – apangin

+0

テストを2つのファイルに分割して実行したとき、同じ結果が維持されました。私のコードは少し早く(49.7秒)実行され、もう一方のコードももう少し速く(111.6秒)実行されましたが、それはまだ2つの方法の劇的な違いです。 –

-1

TL; DR

あなたの実験は無効です。結果に影響する可能性のある変数が多数あります。 CaliperやJMHなどのマイクロベンチマーキングツールを使用することをお勧めします。私はあなたとあなたの教授の間の差はごくわずかであるwhich method is faster to create indentation

をチェックするため、このようなツールを使用しました。

実験

私の実験では、私は745,038データポイントを持っていました。 3つのテスト、あなたのインストラクターのバージョン、Arrays.sort()をJDKの一部として作成しました。だから我々は、約0.01ミリ秒を話している1,429,798.824 NS

: あなたのインストラクターだった1,419,867.808 NS:あなたのランタイムはした結果に基づいて

https://microbenchmarks.appspot.com/runs/8b8c0554-d3f1-4339-af5a-fdffd18dd053

インストラクターは、実行間の差がわずかです。

JDK Arrays.sort()は、より大きなサイズである1,779,042.513 nsで遅く、これは0.300ms遅いです。

以下は、私が以下のCaliperでマイクロベンチマークを行うために使用したコードです。

別に
package net.trajano.caliper.test; 

import java.io.DataInputStream; 
import java.io.EOFException; 
import java.io.IOException; 
import java.nio.file.Files; 
import java.nio.file.Paths; 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 

import com.google.caliper.BeforeExperiment; 
import com.google.caliper.Benchmark; 
import com.google.caliper.api.VmOptions; 
import com.google.caliper.runner.CaliperMain; 

@VmOptions("-XX:-TieredCompilation") 
public class SortBenchmark { 

    public static int[] insertionSort(final int[] A) { 

     // Check for illegal cases 
     if (A == null || A.length == 0) { 

      throw new IllegalArgumentException("A is not populated"); 

     } 

     for (int i = 0; i < A.length; i++) { 

      int j = i; 

      while (j > 0 && A[j - 1] > A[j]) { 

       final int temp = A[j - 1]; 
       A[j - 1] = A[j]; 
       A[j] = temp; 

       j--; 

      } 

     } 

     return A; 

    } 

    public static int[] insertionSortInstructor(final int[] A) { 

     // Check for illegal cases 
     if (A == null || A.length == 0) { 

      throw new IllegalArgumentException("A is not populated"); 

     } 

     for (int i = 0; i < A.length; i++) { 

      int j = i; 

      while (j > 0 && A[j - 1] > A[j]) { 

       final int temp = A[j]; 
       A[j] = A[j - 1]; 
       A[j - 1] = temp; 

       j--; 

      } 

     } 

     return A; 

    } 

    @BeforeExperiment 
    void setUp() throws IOException { 
     try (final DataInputStream dis = new DataInputStream(
       Files.newInputStream(Paths.get("C:/Program Files/iTunes/iTunes.exe")))) { 
      final List<Integer> list = new ArrayList<Integer>(); 
      while (true) { 
       try { 
        list.add(dis.readInt()); 
       } catch (final EOFException e) { 
        break; 
       } 
      } 

      data = list.stream().mapToInt(i -> i).toArray(); 
      System.out.println("Data size = " + data.length); 
     } 
    } 

    // data to sort 
    private static int[] data; 

    @Benchmark 
    public void insertionSort(final int reps) { 
     for (int i = 0; i < reps; i++) { 
      insertionSort(data); 
     } 
    } 

    @Benchmark 
    public void insertionSortInstructor(final int reps) { 
     for (int i = 0; i < reps; i++) { 
      insertionSortInstructor(data); 
     } 
    } 

    @Benchmark 
    public void jdkSort(final int reps) { 
     for (int i = 0; i < reps; i++) { 
      Arrays.sort(data); 
     } 
    } 

    public static void main(final String[] args) { 
     CaliperMain.main(SortBenchmark.class, args); 
    } 
} 

私はJDKが遅かったこと、その結果に驚いた正直に言うと。だから私はthe source codeを見ました。しきい値に応じてJDKで使用される3つのソートアルゴリズムがあるようです(マージソート、286未満のクイックソート、47未満のエレメントの挿入ソート)。

私が持っていたデータセットはかなり大きかったので、マージソートは最初にO(n)スペースの複雑さを持っていて、配列の2番目のコピーの形になっています。このように余分なヒープ割り当てが余分な時間を引き起こしている可能性があります。

+0

私はMacOS 10.11 Java 8u77b03で 'javac'コンパイラを使ってテストコードをもう一度実行しましたが、これは私の結果です(私のコードは私の講師のコードよりもはるかに速いです): '私の挿入の種類は56634.0ミリ秒でした。 その他の種類の挿入には146979.0ミリ秒かかりました。 –

0

を使用して得られました。このようなことは、機械命令(ジャストインタイムJITコンパイラによって作成されたもの)またはマイクロプロセッサ(特にプロセッサキャッシュの1つの効果)のいずれかの影響を受けることがあります。

このソートアルゴリズムでは、ループの各繰り返しに対して、A [j-1]が最後の繰り返しでロードされたため、A [j]だけをロードする必要があることに注意してください。したがって、最近ロードされた値はA [j]でした(私がちょうどあるレベルで利用されたという事実を仮定して)。したがって、ループ内でA [j]を最初に格納するアルゴリズムは、その値がループチェックによって最後にロードされたため、レジスタまたはプロセッサキャッシュに残る可能性が高く、 JITによって生成されたマシンコードの最適化のために、1つのメモリ負荷に制限されます。

実行中のマシン命令のシリーズを正確に知ることは非常に困難です(JITはいくつかの異なるレベルのコンパイルを持っていますが、それぞれ異なる最適化があります。相互作用する可能性がある多くの異なるJIT最適化があり、異なる機械コードをもたらす)。また、どのメモリアクセスがRAMに送られなければならないのか、プロセッサ内のメモリキャッシュヒットによって回避されるのかを知ることは難しい。上記の効果のいくつかの組み合わせがここでのパフォーマンスに影響を与えることは間違いありません。アルゴリズムの順序がパフォーマンスに最も影響しますが、それを超えるとJITやマイクロプロセッサにはパフォーマンスに影響を与える可能性がある多くの最適化があります。

私の本能は、プロセッサのメモリキャッシュが一連の操作でうまくいっていることにあります。これは、A [j]だけを各ループの繰り返しでロードする必要があるという事実によるものです。

関連する問題