Java 为什么HashMaps 10 get()调用的性能不是单个get()性能的10倍

Java 为什么HashMaps 10 get()调用的性能不是单个get()性能的10倍,java,performance,hashmap,jvm,benchmarking,Java,Performance,Hashmap,Jvm,Benchmarking,我有一个hashmap字段,其中包含100_000条目和两个方法a和B 方法A:使用随机键调用map.get()一次 方法B:使用a随机键调用map.get() 我用JMH将这两种方法配置为运行多次(整个测试在我的Macbook pro上花费了1个多小时)并测量吞吐量,结果发现方法A的速度只有方法B的两倍。我希望相差10倍 我无法解释这种行为,以下是结果 # Run complete. Total time: 01:08:36 Benchmark

我有一个hashmap字段,其中包含
100_000
条目和两个方法a和B

  • 方法A:使用随机键调用
    map.get()
    一次
  • 方法B:使用a随机键调用
    map.get()
我用
JMH
将这两种方法配置为运行多次(整个测试在我的Macbook pro上花费了1个多小时)并测量吞吐量,结果发现方法A的速度只有方法B的两倍。我希望相差10倍

我无法解释这种行为,以下是结果

# Run complete. Total time: 01:08:36

Benchmark                           Mode  Throughput           Units
TestClass.benchmark_with_one_get   thrpt   23819.007   operations/ms
TestClass.benchmark_with_ten_gets  thrpt   12021.025   operations/ms
然后我想做更多的实验,我用一个常量函数(总是返回整数
5
)覆盖hashmaps键(
TestKey
)的
hashCode()
方法,使hashmap本质上成为一个列表。只有我才能看到我所期待的结果。我没有运行整个测试,因为它需要6个多小时才能完成,但这里是粗略的结果(仅来自前2次迭代)

这是我用来做基准测试的原始类

package test.benchmark;

import org.openjdk.jmh.annotations.*;

import java.util.*;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
public class TestClass {

    private Map<TestKey, Integer> map = new HashMap<>();
    private List<TestKey> keyList = new ArrayList<>();
    private int test1 = 0;
    private int test2 = 0;

    public TestClass() {
        Random random = new Random();
        for (int i = 0; i < 100_000; i++) {
            TestKey testKey = new TestKey(i);
            map.put(testKey, i);
            keyList.add(new TestKey(random.nextInt(100_001)));
        }
    }

    @Setup
    public void setup() {
        Random random = new Random();
        int tmp = random.nextInt(100_001);
        test1 = tmp;
        test2 = tmp;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 20, time = 10)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Warmup(iterations = 5)
    public boolean benchmark_with_one_get() {

        TestKey testKey = keyList.get(test1);
        map.get(testKey);
        test1++;
        if (test1 >= 100_000) {
            test1 = 0;
        }
        return true;
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Measurement(iterations = 20, time = 10)
    @BenchmarkMode(Mode.Throughput)
    @Warmup(iterations = 5)
    public boolean benchmark_with_ten_gets() {

        TestKey testKey = keyList.get(test2);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        test2++;
        if (test2 >= 100_000) {
            test2 = 0;
        }

        return true;
    }


    public class TestKey {
        private int key;

        public TestKey(int key) {
            this.key = key;
        }

        public int getKey() {
            return key;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof TestKey && ((TestKey) obj).key == this.key;
        }

        @Override
        public int hashCode() {
            return 31 * 17 + key;
        }

    }
}

package test.benchmark;
导入org.openjdk.jmh.annotations.*;
导入java.util.*;
导入java.util.concurrent.TimeUnit;
@国家(范围、基准)
公共类TestClass{
私有映射映射=新的HashMap();
private List keyList=new ArrayList();
私有int test1=0;
私有int test2=0;
公共测试类(){
随机=新随机();
对于(int i=0;i<100_000;i++){
TestKey TestKey=新的TestKey(i);
map.put(testKey,i);
添加(新的TestKey(random.nextInt(100_001));
}
}
@设置
公共作废设置(){
随机=新随机();
int tmp=随机。下一个(100_001);
test1=tmp;
test2=tmp;
}
@基准
@基准模式(模式吞吐量)
@测量(迭代次数=20次,时间=10次)
@OutputTimeUnit(时间单位毫秒)
@预热(迭代次数=5次)
公共布尔基准测试_与_one _get(){
TestKey=keyList.get(test1);
get(testKey);
test1++;
如果(测试1>=100_000){
test1=0;
}
返回true;
}
@基准
@OutputTimeUnit(时间单位毫秒)
@测量(迭代次数=20次,时间=10次)
@基准模式(模式吞吐量)
@预热(迭代次数=5次)
公共布尔基准测试_与_-ten_-get(){
TestKey=keyList.get(test2);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
get(testKey);
test2++;
如果(测试2>=100_000){
test2=0;
}
返回true;
}
公共类测试密钥{
私钥;
公共测试密钥(int密钥){
this.key=key;
}
public int getKey(){
返回键;
}
@凌驾
公共布尔等于(对象obj){
返回TestKey&((TestKey)obj)的obj实例。key==this.key;
}
@凌驾
公共int hashCode(){
返回31*17+键;
}
}
}

欢迎提出任何意见和解释。谢谢

有点令人失望,JVM没有完全优化访问,因为您没有使用
map.get(testKey)的结果

或至少对同一函数进行多次调用。可能在单独的
map.get调用的引擎盖下发生了一些常见的子表达式消除。e、 g.
testKey
上的
hashCode()
方法的结果至少可以用于每次调用

或者这些都不会发生

整个效果很容易用缓存位置来解释:第一次访问很昂贵,因为它可能在缓存中丢失。以后的访问非常便宜:重新加载刚加载的内容可能会在L1d缓存中命中。无序执行可以交错所有这些独立的工作,因此对相同结果的10次“等待”基本上是并行的,这取决于JIT优化完成时每个调用实际运行的本机代码数量。(例如,Skylake CPU的重排序缓冲区大小为224 uops。)

使用相同的密钥访问相同的哈希将访问相同的内存位置


使哈希映射退化从而变成链表搜索意味着每次访问都需要很长的时间,超过了无序的执行窗口大小,因此即使是现代高端CPU也无法发现和利用指令级并行性并交错工作

这还意味着您在遍历该链表时接触了太多内存,以至于在到达末尾时,该链表的开头在缓存中还不是很热。因此,以后的遍历不会受益于已经“开辟了一条路径”并在缓存中获得了热数据

遍历缓存中不热的链表对CPU来说是非常糟糕的。在知道正确的地址之前,它无法开始下一次加载,但这取决于它等待的加载。因此,一次只能运行一个负载,没有内存级别的并行性

(与循环执行<代码>数组[i++]
的数组不同,在数据仍在传输的情况下,循环执行<代码>数组[i++]
可以便宜地计算下一个地址。Skylake等现代x86有12个“行填充缓冲区”;它可以并行跟踪不同缓存线输入/输出的12个未完成请求。如果每次访问+使用的代码足够短,使得无序exec可以在第二次启动,而第一次仍在运行中,则使用不同密钥对非退化hashmap进行多次访问可以利用这一点。(分支预测+推测)
package test.benchmark;

import org.openjdk.jmh.annotations.*;

import java.util.*;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
public class TestClass {

    private Map<TestKey, Integer> map = new HashMap<>();
    private List<TestKey> keyList = new ArrayList<>();
    private int test1 = 0;
    private int test2 = 0;

    public TestClass() {
        Random random = new Random();
        for (int i = 0; i < 100_000; i++) {
            TestKey testKey = new TestKey(i);
            map.put(testKey, i);
            keyList.add(new TestKey(random.nextInt(100_001)));
        }
    }

    @Setup
    public void setup() {
        Random random = new Random();
        int tmp = random.nextInt(100_001);
        test1 = tmp;
        test2 = tmp;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 20, time = 10)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Warmup(iterations = 5)
    public boolean benchmark_with_one_get() {

        TestKey testKey = keyList.get(test1);
        map.get(testKey);
        test1++;
        if (test1 >= 100_000) {
            test1 = 0;
        }
        return true;
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Measurement(iterations = 20, time = 10)
    @BenchmarkMode(Mode.Throughput)
    @Warmup(iterations = 5)
    public boolean benchmark_with_ten_gets() {

        TestKey testKey = keyList.get(test2);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        map.get(testKey);
        test2++;
        if (test2 >= 100_000) {
            test2 = 0;
        }

        return true;
    }


    public class TestKey {
        private int key;

        public TestKey(int key) {
            this.key = key;
        }

        public int getKey() {
            return key;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof TestKey && ((TestKey) obj).key == this.key;
        }

        @Override
        public int hashCode() {
            return 31 * 17 + key;
        }

    }
}