当前位置: 首页 > 知识库问答 >
问题:

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

叶翰林
2023-03-14

我的最终目标是使用标准Java集合作为基准,为几个Java基本集合库创建一套全面的基准。在过去,我曾使用循环方法编写这类微基准测试。我将我正在进行基准测试的函数放在一个循环中,并迭代100万次,这样jit就有机会预热。我取循环的总时间,然后除以迭代次数,得到一个对我正在进行基准测试的函数的单个调用所需时间的估计值。在最近阅读了JMH项目,特别是这个例子:JMHSample_11_循环之后,我看到了这种方法的问题。

我的机器:

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纳秒。二级缓存引用大约为7纳秒,但具有1000万长键和值的哈希映射远远超过二级缓存。而且JMH的结果对我来说很奇怪。90%的get通话需要0.0纳秒?

我假设这是用户错误。如有任何帮助/建议,将不胜感激。谢谢

使现代化

下面是执行AverageTimerun的结果。这更符合我的期望。谢谢@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

共有1个答案

卫阳炎
2023-03-14

首先,循环测试测量平均时间,而JMH代码配置为采样时间。从模式。SampleTimejavadoc:

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

Map的单独执行。get()非常快,由于时间度量粒度的原因,底层时间度量系统会在某些执行中报告0(有关更多信息,请阅读JMH作者发表的Nanotrusting Nanotime博客文章)。

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

>

  • 您应该使用模式。AverageTime(或模式。吞吐量)基准测试模式。离开模式。SampleTime用于单个调用需要大量时间的情况。

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

    你可以尝试使用黑洞。consumeCPU()以增加单个调用的执行时间(请参阅前面关于“基线”和相关陷阱的一点)。

  •  类似资料:
    • 我使用JMH对DOM解析器进行基准测试。我得到了非常奇怪的结果,因为第一次迭代实际上比后面的迭代运行得更快 有人能解释为什么会这样吗?此外,百分位数和所有数字意味着什么?为什么在第三次迭代后它开始变得稳定?一次迭代是否意味着整个基准测试方法的一次迭代?下面是我正在运行的方法

    • 受另一个关于堆栈溢出的问题的启发,我编写了一个微型基准来检查,什么更有效: 有条件地检查零除数或 捕获和处理 下面是我的代码: 我对JMH完全陌生,不确定代码是否正确。 我的基准是正确的吗?你看到任何错误吗? 旁白:请不要建议询问https://codereview.stackexchange.com.对于Codereview,代码必须已按预期工作。我不确定这个基准是否能按预期工作。

    • 我正在使用向服务器发送REST请求的自定义Java库为HTTP服务器编写性能测试。在开始时,我正在执行数据准备阶段,以便获得一个要向服务器发送请求的对象列表。 现在,问题是,我可以使用注释测试可以注入基准函数的参数列表: 问题是,我希望通过Java参数列表实现同样的效果,并避免对它们进行迭代。你能告诉我怎么做吗?

    • 我是JMH的新手。在运行代码并使用不同的注释之后,我真的不明白它是如何工作的。我使用迭代=1、预热=1、fork=1来查看我的代码将执行一次,但事实并非如此。JMH运行我的代码超过100000次,我不知道为什么。那么,如何控制代码调用的时间?下面是我的代码:(我为测试修复了JMHSample\u 01)

    • 我目前有一个JMH基准测试来衡量所有实现相同接口的各种数据结构的性能。基准测试工作正常,但我想为每个基准测试打印一些额外的信息,这些信息描述了我的数据结构在试验前后的状态。 目前,我正在做下面的代码 问题是与JMH的输出混淆。有没有办法避免这种情况,并在JMH完成其试验报告后运行我的打印声明?另一种方法是将其写入一个文件,然后进行合并,但最好得到一个不需要手动合并的报告。

    • 我还没能找到任何真正能回答我问题的答案,所以就这样吧。 我编写了一个Java库,我希望使用一个独立的Java项目来执行基准测试,该项目将JMH作为依赖项。我能够编写一个执行基准测试的基准测试方法,但是这还不够详细,不能满足我的需要。我想知道图书馆的哪些方法占用的时间最多。通过这种方式,我更好地了解了可能的瓶颈,以及从哪里开始优化我的库。 这意味着我希望有一个详细的树报告,其中显示了底层方法调用以及