web-dev-qa-db-ja.com

Javaベンチマーク-なぜ2番目のループの方が速いのですか?

私はこれについて興味があります。

どちらの関数が速いかを確認したかったので、小さなコードを作成して何度も実行しました。

public static void main(String[] args) {

        long ts;
        String c = "sgfrt34tdfg34";

        ts = System.currentTimeMillis();
        for (int k = 0; k < 10000000; k++) {
            c.getBytes();
        }
        System.out.println("t1->" + (System.currentTimeMillis() - ts));

        ts = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            Bytes.toBytes(c);
        }
        System.out.println("t2->" + (System.currentTimeMillis() - ts));

    }

「2番目の」ループの方が速いので、hadoopのBytesクラスはStringクラスの関数よりも速いと思いました。次に、ループの順序を変更し、c.getBytes()を高速化しました。私は何度も実行しましたが、私の結論は理由がわかりませんでしたが、最初のコードが実行された後、VMで何かが発生し、2番目のループの結果が速くなります。

49
Guille

これは古典的なJavaベンチマークの問題です。Hotspot/ JIT/etcは使用中にコードをコンパイルするため、実行中に高速になります。

最初にループを少なくとも3000回実行し(サーバーまたは64ビットで10000)、次に測定を行います。

61
Tim B

Bytes.toBytesが内部でc.getBytesを呼び出すため、何か問題があることがわかります。

public static byte[] toBytes(String s) {
    try {
        return s.getBytes(HConstants.UTF8_ENCODING);
    } catch (UnsupportedEncodingException e) {
        LOG.error("UTF-8 not supported?", e);
        return null;
    }
}

ソースは here から取得されます。これは、呼び出しが直接呼び出しよりも速くなる可能性がないことを示しています-最高の状態で(つまり、インライン化された場合)、同じタイミングになります。ただし、一般的には、関数を呼び出す際のオーバーヘッドが小さいため、少し遅くなることが予想されます。

これは、ガベージコレクターなど、任意の時間に実行されるコンポーネントを含む、解釈されたガベージコレクション環境でのマイクロベンチマークの典型的な問題です。その上で、キャッシュなど、ハードウェアの最適化によって画像が歪められます。その結果、何が起こっているかを確認する最良の方法は、多くの場合、ソースを確認することです。

18
dasblinkenlight

「2番目の」ループの方が速いので、

メソッドを少なくとも10000回実行すると、メソッド全体がトリガーされてコンパイルされます。これは、2番目のループが

  • 初めて実行したときにすでにコンパイルされているため、より高速です。
  • 最適化するとコードの実行方法に関する適切な情報/カウンターがないため、速度が低下します。

最善の解決策は、各ループを個別のメソッドに配置して、1つのループが他のループを最適化せず、最初の実行を無視してこれを数回実行することです。

例えば.

for(int i = 0; i < 3; i++) {
    long time1 = doTest1();  // timed using System.nanoTime();
    long time2 = doTest2();
    System.out.printf("Test1 took %,d on average, Test2 took %,d on average%n",
        time1/RUNS, time2/RUNS);
}
13
Peter Lawrey

ほとんどの場合、コードはまだコンパイルされているか、最初のループが実行されたときにまだコンパイルされていませんでした。

ベンチマークを数回実行できるようにメソッド全体を外側のループでラップすると、より安定した結果が表示されます。

読む: 動的コンパイルとパフォーマンス測定

6
Boann

GetBytes()の呼び出しでオブジェクトに非常に多くのスペースを割り当て、JVMガーベッジコレクターが起動して未使用の参照をクリーンアップする(ゴミ箱を空ける)場合は、単にそうなのかもしれません。

5
reindeer

いくつかのより多くの観察

  • 上記の@dasblinkenlightで指摘されているように、HadoopのBytes.toBytes(c);は内部でString.getBytes("UTF-8")を呼び出します

  • 入力として文字セットを使用するバリアントメソッドString.getBytes()より速い文字セットを使用しないメソッド。したがって、指定された文字列の場合、getBytes("UTF-8")getBytes()よりも高速になります。私は自分のマシン(Windows8、JDK 7)でこれをテストしました。 1つのgetBytes("UTF-8")を使用したループと、もう1つのgetBytes()を使用したループの2つを順番に繰り返し実行します。

    _    long ts;
        String c = "sgfrt34tdfg34";
    
        ts = System.currentTimeMillis();
        for (int k = 0; k < 10000000; k++) {
            c.getBytes("UTF-8");
        }
        System.out.println("t1->" + (System.currentTimeMillis() - ts));
    
        ts = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) { 
            c.getBytes();
        }
        System.out.println("t2->" + (System.currentTimeMillis() - ts));
    _

これは与える:

_t1->1970
t2->2541
_

ループの実行順序を変更しても結果は同じです。 JITの最適化を割り引くには、これを確認するために別の方法でテストを実行することをお勧めします(上記の@Peter Lawreyが提案)。

  • したがって、Bytes.toBytes(c)は常にString.getBytes()より高速である必要があります
1
Santosh