3

hashmap的一些性能测试 - 是奉壹呀

 1 year ago
source link: https://www.cnblogs.com/eryuan/p/17059341.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文主要讨论哈希冲突下的一些性能测试。
为什么要写这篇文章,不是为了KPI不是为了水字数。
hashmap是广大JAVA程序员最为耳熟能详,使用最广泛的集合框架。它是大厂面试必问,著名八股经必备。在小公司呢?这些年也面过不少人,对于3,5年以上的程序员,问到hashmap也仅限于要求知道底层是数组+链表,知道怎么放进去,知道有哈希冲突这么一回事即可,可依然免不了装备的嫌疑。

可hashmap背后的思想,在缓存,在数据倾斜,在负载均衡等分布式大数据领域都能广泛看到其身影。了解其背后的思想不仅仅只是为了一个hashmap.

更重要的是,hashmap不像jvm底层原理那么遥远,不像并发编程那么宏大,它只需要通勤路上十分钟就可搞定基本原理,有什么理由不呢?

所以本文试着从相对少见的一个微小角度来重新审视一下hashmap.

1.准备工作。

1.1模拟哈希冲突

新建两个class,一个正常重写equalshashcode方法,一个故意在hashcode方法里返回一定范围内的随机数,模拟哈希冲突,以及控制哈希冲突的程序。

不冲突的类

@Setter
public class KeyTest2 {
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        KeyTest2 keyTest = (KeyTest2) o;

        return name != null ? name.equals(keyTest.name) : keyTest.name == null;
    }

    @Override
    public int hashCode() {
         return name != null ? name.hashCode() : 0;
    }
}
@Setter
@NoArgsConstructor
public class KeyTest {
    private String name;

    private Random random;

    public KeyTest(Random random){
        this.random = random;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        KeyTest keyTest = (KeyTest) o;

        return name != null ? name.equals(keyTest.name) : keyTest.name == null;
    }

    @Override
    public int hashCode() {
        // return name != null ? name.hashCode() : 0;
        return random.nextInt(1000);
    }
}

众所周知,hashmap在做put的时候,先根据key求hashcode,找到数组下标位置,如果该位置有元素,再比较equals,如果返回true,则替换该元素并返回被替换的元素;否则就是哈希冲突了,即hashcode相同但equals返回false。
哈希冲突的时候在冲突的数组处形成数组,长度达到8以后变成红黑树。

1.2 java的基准测试。

这里使用JMH进行基准测试.
JMH是Java Microbenchmark Harness的简称,一般用于代码的性能调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。和 Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,而不仅限于rest api.

jdk9以上的版本自带了JMH,如果是jdk8可以使用maven引入依赖。

点击查看JMH依赖

<dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
        </dependency>

2.测试初始化长度

点击查看初始化长度基本测试代码

/使用模式 默认是Mode.Throughput
@BenchmarkMode(Mode.AverageTime)
// 配置预热次数,默认是每次运行1秒,运行10次,这里设置为3次
@Warmup(iterations = 3, time = 1)
// 本例是一次运行4秒,总共运行3次,在性能对比时候,采用默认1秒即可
@Measurement(iterations = 3, time = 4)
// 配置同时起多少个线程执行
@Threads(1)
//代表启动多个单独的进程分别测试每个方法,这里指定为每个方法启动一个进程
@Fork(1)
// 定义类实例的生命周期,Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
@State(value = Scope.Benchmark)
// 统计结果的时间单元
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HashMapPutResizeBenchmark {

    @Param(value = {"1000000"})
    int value;

    /**
     * 初始化长度
     */
    @Benchmark
    public void testInitLen(){
        HashMap map = new HashMap(1000000);
        Random random = new Random();
        for (int i = 0; i < value; i++) {
            KeyTestConflict test = new KeyTestConflict(random, 10000);
            test.setName(i+"");
            map.put(test, test);
        }
    }

    /**
     * 不初始化长度
     */
    @Benchmark
    public void testNoInitLen(){
        HashMap map = new HashMap();
        for (int i = 0; i < value; i++) {
            Random random = new Random();
            KeyTestConflict test = new KeyTestConflict(random, 10000);
            test.setName(i+"");
            map.put(test, test);
        }
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(HashMapPutResizeBenchmark.class.getSimpleName())
                .mode(Mode.All)
                // 指定结果以json结尾,生成后复制可去:http://deepoove.com/jmh-visual-chart/ 或https://jmh.morethan.io/ 得到可视化界面
                .result("hashmap_result_put_resize.json")
                .resultFormat(ResultFormatType.JSON).build();

        new Runner(opt).run();
    }
}

测试结果图

600147-20230118145522435-1406214306.png

对测试结果图例做一个简单的说明:

以上基准测试,会得到一个json格式的结果。然后将该结果上传到官方网站,会得到一个上述图片的结果。
横坐标,红色驻图代表有冲突,浅蓝色驻图无冲突。
众坐标,ops/ns代表平均每次操作花费的时间,单位为纳秒,1秒=1000000000纳秒,这样更精准。
下同。

简单说,驻图越高代表性能越低。

我测了两次,分别是无哈希冲突和有哈希冲突的,这里只贴一种结果。

测试结果表明,hashmap定义时有初始化对比无初始化,有大约4%到12%的性能损耗。

足够的初始化长度下,有哈希冲突的测试结果:

600147-20230118151324717-1448471715.png

足够的初始化长度下,没有哈希冲突的测试结果:

600147-20230118154001032-709833173.png

3.模拟一百万个元素put,get的差异。

600147-20230118142037932-1830782414.png

众所周知,hashmap在频繁做resize时,性能损耗非常严重。以上是没初始化长度,无冲突和有冲突的情况下,前者性能是后者性能的53倍。

那么在初始化长度的情况下呢?

HashMap map = new HashMap(1000000);

同样的代码下,得到的测试结果

600147-20230118143117632-2100671133.png

以上是有初始化长度,无冲突和有冲突的情况下,前者性能是后者性能的58倍。

大差不差,不管有无初始化长度,无冲突的效率都是有冲突效率的50倍以上。说明,这是哈希冲突带来的性能损耗。

4.模拟无红黑树情况下get效率

4.1 将random扩大,哈希冲突严重性大大减小,模拟大多数哈希冲突导致的哈希链长度均小于8,无法扩展为红黑树,只能遍历数组。

将KeyTest的hashcode方法改为:

@Override
    public int hashCode() {
        // return name != null ? name.hashCode() : 0;
        return random.nextInt(130000);
    }

这样1000000/130000 < 8,这样大多数的哈希链将不会扩展为红黑树。

测试结果为:

600147-20230118141303847-714323792.png

测试结果说明,**有冲突的效率反而比无冲突的效率要高**,差不多高出80%左右。
这其实有点违反常识,我们通常讲,hashmap要尽量避免哈希冲突,哈希冲突的情况下写入和读取性能都会受到很大的影响。
但是上面的测试结果表明,大数据量相对比较大的时候,适当的哈希冲突(<8)反而读取效率更高。
个人猜测是,适当的哈希冲突,数组长度大为减少。

为了证明以上猜想,直接对ArrayList进行基准测试。

4.1.1 ArrayList不同长度下get效率的基准测试

模拟一个哈希冲突非常严重下,底层数组长度较小的list,和哈希冲突不严重情况下,底层数组较大的list,再随机测试Get的效率如何。

点击查看测试代码

//使用模式 默认是Mode.Throughput
@BenchmarkMode(Mode.AverageTime)
// 配置预热次数,默认是每次运行1秒,运行10次,这里设置为3次
@Warmup(iterations = 3, time = 1)
// 本例是一次运行4秒,总共运行3次,在性能对比时候,采用默认1秒即可
@Measurement(iterations = 3, time = 4)
// 配置同时起多少个线程执行
@Threads(1)
//代表启动多个单独的进程分别测试每个方法,这里指定为每个方法启动一个进程
@Fork(1)
// 定义类实例的生命周期,Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
@State(value = Scope.Benchmark)
// 统计结果的时间单元
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ArrayListGetBenchmark {

    //    @Param(value = {"1000","100000","1000000"})
    @Param(value = {"1000000"})
    int value;


    @Benchmark
    public void testConflict(){
        int len = 10000;
        Random random = new Random(len);
        for (int i = 0; i < 100; i++) {
            int index = random.nextInt(len);
            System.out.println("有冲突,index = " + index);
            ConflictHashMapOfList.list.get(index);
        }
    }

    @Benchmark
    public void testNoConflict(){
        int len = 1000000;
        Random random = new Random(len);
        for (int i = 0; i < 100; i++) {
            int index = random.nextInt(len);
            System.out.println("无冲突,index = " + index);
            NoConflictHashMapOfList.list.get(index);
        }
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(HashMapGetBenchmark.class.getSimpleName())
                .mode(Mode.All)
                // 指定结果以json结尾,生成后复制可去:http://deepoove.com/jmh-visual-chart/ 或https://jmh.morethan.io/ 得到可视化界面
                .result("arraylist_result_get_all.json")
                .resultFormat(ResultFormatType.JSON).build();

        new Runner(opt).run();
    }


    @State(Scope.Thread)
    public static class ConflictHashMapOfList {
        volatile static ArrayList list = new ArrayList();
        static int randomMax = 10000;
        static {
            // 模拟哈希冲突严重,数组长度较小
            for (int i = 0; i < randomMax; i++) {
                list.add(i);
            }
        }

    }

    @State(Scope.Thread)
    public static class NoConflictHashMapOfList {
        volatile static ArrayList list = new ArrayList();
        static int randomMax = 1000000;
        static {
            // 模拟没有哈希冲突,数组长度较大
            for (int i = 0; i < randomMax; i++) {
                list.add(i);
            }
        }

    }
}

测试结果如下:

600147-20230128101658006-750990292.png

可以看到,间接证实了以上的猜想。
当然这里的代码可能并不严谨,也欢迎大家一起讨论。

4.2 jdk1.8版本,哈希冲突严重下的get效率测试

600147-20230118140506067-1847310007.png

测试结果说明:在jdk8,无冲突效率是有有冲突的3倍左右。

4.3 将jdk版本降为1.7,在哈希冲突依然严重的情况下,get效率如何?

600147-20230118134711731-1952692314.png

测试结果说明:在jdk7,无冲突效率是有有冲突的12倍左右。

结合4.1和4.2的测试对比,说明jdk1.8红黑树的优化效率确实提升很大。

1.初始化的时候指定长度,长度要考虑到负载因子0.75.初始化的影响受到哈希冲突的影响,没有那么大(相对于倍数而言),但也不小。
2.哈希冲突严重时,put性能急剧下降。(几十倍级)
3.相同元素个数的前提下,在哈希冲突时,get效率反而更高。
4.相比之前的版本,哈希冲突严重时,jdk8红黑树对get效率有非常大的提升。

测试代码和测试结果在 这里


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK