10

私の最終的な目標は、標準の Java コレクションをベースラインとして使用して、いくつかの Java プリミティブ コレクション ライブラリの包括的なベンチマーク セットを作成することです。過去に、私はこれらの種類のマイクロベンチマークを作成するループ方法を使用しました。ベンチマークしている関数をループに入れて 100 万回以上反復するので、jit はウォームアップする機会があります。ループの合計時間を取り、反復回数で割って、ベンチマークしている関数への 1 回の呼び出しにかかる時間を見積もります。最近、JMHプロジェクト、具体的には次の例について読んだ後: JMHSample_11_Loopsこのアプローチの問題を確認しました。

私のマシン:

Windows 7 64-bit
Core i7-2760QM @ 2.40 GHz
8.00 GB Ram
jdk1.7.0_45 64-bit

上記のループ メソッド コードの簡略化された単純な例を次に示します。

    public static void main(String[] args) {
    HashMap<Long, Long> hmap = new HashMap<Long, Long>();
    long val = 0;

    //populating the hashmap
    for (long idx = 0; idx < 10000000; idx++) {
        hmap.put(idx, idx);
    }


    Stopwatch s = Stopwatch.createStarted();
    long x = 0;
    for (long idx = 0; idx < 10000000; idx++) {
       x =  hmap.get(idx);
    }
    s.stop();
    System.out.println(s); //5.522 s
    System.out.println(x); //9999999

    //5.522 seconds / 10000000 = 552.2 nanoseconds
}

JMH を使用してこのベンチマークを書き直そうとしているのは次のとおりです。

package com.test.benchmarks;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;


@State(Scope.Thread)
public class MyBenchmark {


    private HashMap<Long, Long> hmap = new HashMap<Long, Long>();
    private long key;

    @Setup(Level.Iteration)
    public void setup(){

        key = 0;

        for(long i = 0; i < 10000000; i++) {
            hmap.put(i, i);
        }
    }


    @Benchmark
    @BenchmarkMode(Mode.SampleTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public long testGetExistingKey() throws InterruptedException{

        if(key >= 10000000) key=0;
        return hmap.get(key++);
    }


    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(".*" + MyBenchmark.class.getSimpleName() + ".*")
                .warmupIterations(5)
                .measurementIterations(25)
                .forks(1)
                .build();

        new Runner(opt).run();

    }

}

結果は次のとおりです。

 Result: 31.163 ±(99.9%) 11.732 ns/op [Average]
   Statistics: (min, avg, max) = (0.000, 31.163, 939008.000), stdev = 1831.428
   Confidence interval (99.9%): [19.431, 42.895]
  Samples, N = 263849
        mean =     31.163 ±(99.9%) 11.732 ns/op
         min =      0.000 ns/op
  p( 0.0000) =      0.000 ns/op
  p(50.0000) =      0.000 ns/op
  p(90.0000) =      0.000 ns/op
  p(95.0000) =    427.000 ns/op
  p(99.0000) =    428.000 ns/op
  p(99.9000) =    428.000 ns/op
  p(99.9900) =    856.000 ns/op
  p(99.9990) =   9198.716 ns/op
  p(99.9999) = 939008.000 ns/op
         max = 939008.000 ns/op


# Run complete. Total time: 00:02:07

Benchmark                                Mode   Samples        Score  Score error    Units
c.t.b.MyBenchmark.testGetExistingKey   sample    263849       31.163       11.732    ns/op

私が知る限り、JMH の同じベンチマークでは、ハッシュマップが31ナノ秒であるのに対し、ループ テストでは552ナノ秒です。31 ナノ秒は、私には少し速すぎるようです。すべてのプログラマーが知っておくべきレイテンシの数値を見るメインメモリの参照は約 100 ナノ秒です。L2 キャッシュ参照は約 7 ナノ秒ですが、1,000 万の Long キーと値を持つ HashMap は L2 をはるかに上回ります。また、JMH の結果は奇妙に見えます。get 呼び出しの 90% に 0.0 ナノ秒かかる?

これはユーザーエラーだと思います。ヘルプ/ポインタをいただければ幸いです。ありがとう。

アップデート

実行した結果は次のとおりAverageTimeです。これは私の期待とより一致しています。ありがとう@oleg-estekhin!以下のコメントで、私はAverageTime以前にテストを行い、SampleTime. その実行を行うには、はるかに少ないエントリで HashMap を使用し、ルックアップの高速化は理にかなっているはずです。

Result: 266.306 ±(99.9%) 139.359 ns/op [Average]
  Statistics: (min, avg, max) = (27.266, 266.306, 1917.271), stdev = 410.904
  Confidence interval (99.9%): [126.947, 405.665]


# Run complete. Total time: 00:07:17

Benchmark                                Mode   Samples        Score  Score error    Units
c.t.b.MyBenchmark.testGetExistingKey     avgt       100      266.306      139.359    ns/op
4

1 に答える 1

11

まず、JMH コードがサンプル時間用に構成されている間、ループ テストは平均時間を測定します。Mode.SampleTimejavadocから:

サンプル時間: 各操作の時間をサンプリングします。

の個々の実行はMap.get()、時間測定の粒度により、基礎となる時間測定システムが一部の実行について 0 を報告する時点までかなり高速です (詳細については、JMH の著者によるNanotimeブログ投稿をNanotrustingを参照してください)。

サンプル モードでは、ベンチマークは個々のサンプル時間を配列に収集し、その配列を使用して平均とパーセンタイルを計算します。配列値の半分以上がゼロの場合 (特定の設定では、 で示されるように配列値の 90% 以上がゼロですp(90.0000) = 0.000 ns/op)、平均はかなり低くなりますが、出力にp(50) = 0(特に) が表示されると、p(90) = 0信頼できる唯一の結論は、これらの結果はガベージであり、そのコードを測定する別の方法を見つける必要があるということです。

  • Mode.AverageTime(またはMode.Throughput) ベンチマーク モードを使用する必要があります。Mode.SampleTime個々の呼び出しにかなりの時間がかかる場合は、そのままにしておきます。

  • 簿記に必要な時間と実際の時間を分離するためにif ()andを実行する「ベースライン」ベンチマークを追加することもできますが、結果を説明する必要があります (上記のリンクのブログ投稿では、「実際の時間から「ベースライン」を差し引くことの落とし穴について説明しています。 "測定)。key++keyMap.get()

  • 個々の呼び出しの実行時間を増やすために使用することができBlackhole.consumeCPU()ます (「ベースライン」と関連する落とし穴に関する前のポイントを参照してください)。

于 2014-07-01T06:45:39.603 に答える