10

我的最终目标是使用标准 Java 集合作为基准,为几个 Java 原始集合库创建一套全面的基准测试。过去我使用循环的方法来编写这些微基准。我将我正在基准测试的函数放在一个循环中并迭代 100 万次以上,这样 jit 就有机会预热。我取循环的总时间,然后除以迭代次数,以估计对我进行基准测试的函数的单次调用所花费的时间。在最近阅读了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 中的相同基准测试的 hashmap 为31纳秒,而循环测试为552纳秒。31 纳秒对我来说似乎有点太快了。查看每个程序员都应该知道的延迟数主内存引用大约是 100 纳秒。L2 缓存引用大约是 7 纳秒,但是具有 1000 万个 Long 键和值的 HashMap 远远超过了 L2。JMH 结果对我来说也很奇怪。90% 的 get 调用需要 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 作者的 Nanotrusting Nanotime博客文章以获取更多信息)。

在采样模式下,基准将单个采样时间收集到一个数组中,然后使用该数组计算平均值和百分位数。p(90.0000) = 0.000 ns/op当超过一半的数组值为零时p(50) = 0p(90) = 0在您的特定设置中,超过 90% 的数组值为零,如您唯一可以可靠得出的结论是这些结果是垃圾,您需要找到另一种方法来衡量该代码。

  • 您应该使用Mode.AverageTime(或Mode.Throughput)基准测试模式。离开Mode.SampleTime个人调用需要大量时间的情况。

  • 您可以添加一个执行if ()and的“基线”基准,key++以便隔离key记账所需的时间和实际Map.get()时间,但您需要解释结果(上面链接的博客文章描述了从“真实”中减去“基线”的陷阱“ 测量)。

  • 您可以尝试使用Blackhole.consumeCPU()来增加单个调用的执行时间(请参阅上一点关于“基线”和相关陷阱)。

于 2014-07-01T06:45:39.603 回答