Java HashMap Get 基准测试(JMH 与循环)

2024-03-14

我的最终目标是使用标准 Java 集合作为基线,为多个 Java 原始集合库创建一套全面的基准测试。过去我曾使用循环方法来编写此类微基准。我将要进行基准测试的函数放入循环中并迭代 100 万次以上,以便 jit 有机会预热。我计算循环的总时间,然后除以迭代次数,以估计单次调用我正在进行基准测试的函数所需的时间。最近阅读了有关JMH http://openjdk.java.net/projects/code-tools/jmh/项目,特别是这个例子:JMHSample_11_Loops http://hg.openjdk.java.net/code-tools/jmh/file/3c8d4f23d112/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_11_Loops.java我看到了这种方法的问题。

我的机器:

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纳秒 vs552循环测试的纳秒。 31 纳秒对我来说似乎有点太快了。看着每个程序员都应该知道的延迟数字 https://gist.github.com/jboner/2841832主内存参考时间约为 100 纳秒。 L2 缓存引用大约为 7 纳秒,但具有 1000 万个 Long 键和值的 HashMap 远远超过了 L2。 JMH 结果对我来说也很奇怪。 90% 的 get 调用需要 0.0 纳秒?

我假设这是用户错误。任何帮助/指示将不胜感激。谢谢。

UPDATE

这是执行的结果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

首先,循环测试测量平均时间,而 JMH 代码则配置为采样时间。来自Mode.SampleTimejavadoc:

采样时间:对每个操作的采样时间。

个别处决Map.get()由于时间测量粒度(读纳米信任纳米时间 http://shipilev.net/blog/2014/nanotrusting-nanotime/博客文章由JMH 作者 http://shipilev.net/了解更多信息)。

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

  • 你应该使用Mode.AverageTime (or Mode.Throughput) 基准模式。离开Mode.SampleTime对于单独调用需要大量时间的情况。

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java HashMap Get 基准测试(JMH 与循环) 的相关文章

随机推荐