应用JMH测试大型HashMap的性能
PolarDB初赛进展
写这篇是因为PolarDB比赛很重要的一点是控制内存。C++只有2G,Java也只有3G,而6400W的键值对,即使只是Long
类型,也需要16 * 64 * 10e6 ≈ 1G
的内存,这还不包括其他对象引用的相关开销,所以内存控制在这里是非常重要的,因为稍不小心就会被CGroup无情地kill掉。因此在比赛开始没多久的时候我就研究了一下使用怎样的HashMap可以达到内存最简的状况。在这个过程中,顺便使用了JMH来分析了一下几个侯选库的性能。因为初赛相对来说比较简单,而且HashMap实际上在复赛时候的Range操作上没有发挥余地,所以我决定将这篇写下来分享给大家,希望能帮助更多对比赛有兴趣的同学找到一个比较好的入手点。
之前的初赛简单思路可以看这里。
侯选的集合库
我们能第一时间想到的最朴素最直接的候选者就是Java自带的HashMap
了,这是我们平时使用最多也是最熟悉的实现。只不过在这里因为性能和内存消耗的原因,它稍微有点不合适。其实市面上有很多其他优秀的集合库实现的,我在这里大致列一下我这边会测试的几个:
-
FastUtil: 一个意大利的计算机博士开发的集合库。
-
Eclipse Collections: 由高盛开发的集合库,后来捐给了eclipse基金会,成为了eclipse的项目.
-
HPPC: 专门为原始类型设计的集合库。
-
Koloboke: 又一位大神的作品,目标是低内存高性能。
-
Trove: 挂在bitbucket上面的一个开源项目。
因为是为了比赛而接触的这些库,所以我只按照比赛场景给他们做了测试。我们会预生成6400W对8字节的Key,和8字节的长整型Value,之后会将这些key全部写入各自的HashMap中去,然后再从中读取出来,并与暂存的Value作比较,判断正确性。整个的测试过程是交给JMH来做的。下面介绍一下JMH工具。
JMH简介
JMH是由OpenJDK开发的,用来构建、运行和分析Java或其他Jvm语言所写的程序的基准测试框架。它可以帮助我们自动构建和运行基准测试,并且汇总得到结果。现在一般Java世界里面的主流Benchmark就是应用的JMH。
Scala这边,我们所熟悉的Ktoso大佬包了一个sbt-jmh
插件,使得我们可以方便地利用SBT来运行JMH测试。要使用sbt-jmh
插件,首先,在plugins.sbt
文件里面添加插件:
1// project/plugins.sbt
2addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.4")
之后,在项目中的模块定义中,使用它:
1// build.sbt
2enablePlugins(JmhPlugin)
然后,我们就可以在sbt的console下,执行如下命令,来运行jmh测试了:
1jmh:run -i 3 -wi 3 -f1 -t1 .*FalseSharing.*
上面的参数解释下来,就是要求项目对符合正则表达式.*FalseSharing.*
的基准测试,运行3次,运行之前要进行3次预热,只需要跑一遍,使用一个线程。
好,介绍结束,我们接下来看一下我们如何来编写程序测试各种Map。
HashMap测试代码
首先,我们先创建一个类,如下:
1@State(Scope.Benchmark)
2@OutputTimeUnit(TimeUnit.SECONDS)
3@BenchmarkMode(Array(Mode.Throughput))
4class jmh.HashMapBenchmark {
5
6}
JMH使用注解来标记的基准测试。上面三个注解的选项的意思分别是:
-
State
表明可以在类里面创建成员变量,供所有测试复用,复用的范围是在Benchmark
当中; -
OutputTimeUnit
表示输出Benchmark结果的时候,计时单位是TimeUnit.SECONDS
; -
BenchmarkMode
,Benchmark的模式是测试吞吐率。
为了做基准测试,我们首先创建一个6400W大小的列表,列表的元素是一个二元组,都是Long
:
1 val random = new Random(42)
2
3 val testSet: List[(Long, Long)] = List.range(0, 64000000).map { _ =>
4 val key = random.nextLong()
5 val value = random.nextLong()
6 key -> value
7 }
这里有个关于比赛的小技巧,由于Key都是8字节的,所以其实每个Key都很容易转化成Long
类型的。所以我们在测试里面也只测试对于Long类型的写入性能,以Java的HashMap
为例:
1 @Benchmark
2 @OperationsPerInvocation(OperationsPerInvocation)
3 def testHashMap() = {
4 val hashMap = new util.HashMap[Long, Long](64000000)
5 testSet.foreach { case (k, v) =>
6 hashMap.put(k, v)
7 }
8 testSet.foreach { case (k, v) =>
9 val readValue = hashMap.get(k)
10 assert(readValue == v)
11 }
12 }
@Benchmark
表示这是一个要运行的基准测试。@OperationsPerInvocation
注解会接收一个数值,表示这个测试的方法运行了多少次。在我们这边是OperationPerInvocation
次。注意,前面的变量在jmh.HashMapBenchmark
的伴生对象中定义:
1object jmh.HashMapBenchmark {
2 val OperationsPerInvocation = 64000000 * 2
3}
然后,我们就能够启动sbt,输入前面提到的jmh:run
命令了。我们先跑一波看看:
1jmh:run -i 3 -wi 3 -f1 -t1 .*HashMap.*
跑起来以后我感觉我错了,电脑风扇在狂转,而且预热半天都跑不完。jstat
看一下gc情况试试先,发现100多秒都是FGC。。
1S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
2931840.0 931840.0 0.0 0.0 932352.0 932352.0 5592576.0 5592545.4 9856.0 9391.3 1408.0 1316.6 12 36.611 9 145.669 182.280
3931840.0 931840.0 0.0 0.0 932352.0 932352.0 5592576.0 5592545.4 9856.0 9391.3 1408.0 1316.6 12 36.611 9 145.669 182.280
果断杀掉,加上jvm参数以后再测试:
1jmh:run -i 3 -wi 3 -f1 -t1 --jvmArgs "-Xmx10g" .*HashMap.*
10G也是慢,跑太久了,不乐意跑了,果断放弃。我们直接来看其他的实现。
这里还要说一下,因为内存有要求,所以我们需要同时打印一下HashMap的内存大小。我所使用的是网上找到的一个应该是从Spark代码中抠出来的一个实现,速度快,估值准。只需要在build.sbt
中如下引入即可。
1libraryDependencies += "com.madhukaraphatak" %% "java-sizeof" % "0.1"
主要代码编写
因为其实这里的hashmap的库使用其实大同小异,为了避免重复,所以我利用Scala的一些特性来进行代码编写。首先我定义了一个特质为LongLongOp
:
1trait LongLongOp {
2 def put(key:Long, value:Long):Long
3 def get(key:Long):Long
4}
之后,我们写一个遍历testSet
的函数:
1 def testSetTraverse(hashMap:LongLongOp) = {
2 testSet.foreach { case (k, v) =>
3 hashMap.put(k, v)
4 }
5 testSet.foreach { case (k, v) =>
6 val readValue = hashMap.get(k)
7 assert(readValue == v)
8 }
9 }
然后,我们利用特质可以混入的特性,在生成HashMap的时候,混入LongLongOp
,然后交由testSetTraverse
执行。这样我们每次只需要新建HashMap即可了。例如,FastUtil的测试如下:
1 @Benchmark
2 @OperationsPerInvocation(OperationsPerInvocation)
3 def testFastUtil() = {
4 val map = new Long2LongArrayMap(MapSize) with LongLongOp
5 testSetTraverse(map)
6 printlnObjectSize("fastutil Long2LongArrayMap", map)
7 }
其中printLnObjectSize
是用来打印map
占用内存数量的:
1 def printlnObjectSize(message:String, obj: AnyRef) = {
2 val size = SizeEstimator.estimate(obj)
3 val sizeInGB = size.toDouble / 1024 / 1024 / 1024
4 println(s"$message size is ${sizeInGB}GB")
5 }
之后,依次使用Eclipse Collections, Hppc, Koloboke和Trove,就完成了我们的Benchmark编写:
1import java.util.concurrent.TimeUnit
2
3import com.koloboke.collect.impl.hash.MutableLHashParallelKVLongLongMapGO
4import com.madhukaraphatak.sizeof.SizeEstimator
5import gnu.trove.map.hash.TLongLongHashMap
6import org.openjdk.jmh.annotations._
7
8import scala.util.Random
9
10object HashMapBenchmark {
11 final val OperationsPerInvocation = 64000000 * 2
12}
13
14trait LongLongOp {
15 def put(key: Long, value: Long): Long
16
17 def get(key: Long): Long
18}
19
20@State(Scope.Benchmark)
21@OutputTimeUnit(TimeUnit.SECONDS)
22@BenchmarkMode(Array(Mode.Throughput))
23class HashMapBenchmark {
24
25 import HashMapBenchmark._
26
27 val random = new Random(42)
28 val MapSize = 64000000
29
30 val testSet: List[(Long, Long)] = List.range(0, MapSize).map { _ =>
31 val key = random.nextLong()
32 val value = random.nextLong()
33 key -> value
34 }
35
36 def testSetTraverse(hashMap: LongLongOp) = {
37 testSet.foreach { case (k, v) =>
38 hashMap.put(k, v)
39 }
40 testSet.foreach { case (k, v) =>
41 val readValue = hashMap.get(k)
42 assert(readValue == v)
43 }
44 }
45
46 def printlnObjectSize(message: String, obj: AnyRef) = {
47 val size = SizeEstimator.estimate(obj)
48 val sizeInGB = size.toDouble / 1024 / 1024 / 1024
49 println(s"$message size is ${sizeInGB}GB")
50 }
51
52 @Benchmark
53 @OperationsPerInvocation(OperationsPerInvocation)
54 def testEclipseCollection() = {
55
56 import org.eclipse.collections.impl.map.mutable.primitive.LongLongHashMap
57
58 val map = new LongLongHashMap(MapSize)
59 testSet.foreach { case (k, v) =>
60 map.put(k, v)
61 }
62 testSet.foreach { case (k, v) =>
63 val readValue = map.get(k)
64 assert(readValue == v)
65 }
66 printlnObjectSize("Eclipse LongLongHashMap", map)
67 }
68
69 @Benchmark
70 @OperationsPerInvocation(OperationsPerInvocation)
71 def testHppc() = {
72
73 import com.carrotsearch.hppc.LongLongHashMap
74
75 val map = new LongLongHashMap(MapSize, 0.99) with LongLongOp
76 testSetTraverse(map)
77 printlnObjectSize("hppc LongLongHashMap", map)
78 }
79
80 @Benchmark
81 @OperationsPerInvocation(OperationsPerInvocation)
82 def testKoloboke() = {
83 val map = new MutableLHashParallelKVLongLongMapGO() with LongLongOp
84 testSetTraverse(map)
85 printlnObjectSize("Koloboke", map)
86 }
87
88 @Benchmark
89 @OperationsPerInvocation(OperationsPerInvocation)
90 def testTrove() = {
91 val map = new TLongLongHashMap(MapSize, 1.0f) with LongLongOp
92 testSetTraverse(map)
93 printlnObjectSize("Trove LongLongMap", map)
94 }
95}
其中Eclipse collection的put
方法的返回值是void
,与其他集合不一样,所以只能单独为它写一个测试方法。
结果
运行的过程中,Koloboke报一个诡异的空指针错误,所以没有通过测试;FastUtils在这个量级好像有点慢,不乐意等所以最终没有把它加入测试。最终我们得到如下的结果列表:
综合内存使用以及性能,我个人觉得在此次比赛初赛中,也许HPPC是个比较好的选择。
所以,初赛使用Java的HashMap
实现的小伙伴,是不是应该赶紧思考一下换一下内存索引的结构,来避免OOM呢?
原文发布时间为: 2018-11-07
本文作者:Kirito的技术分享
本文来自云栖社区合作伙伴“Kirito的技术分享”,了解相关信息可以关注“Kirito的技术分享”。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Qt加载QML的2种方式
前言 之所以写这篇文章,是因为在项目中经常会碰到一个问题,qml 文件该如何加载到工程中,其实 Qt Quick APP 有两种模式,另外,还有一种场景是,在 QWidget 界面上加载 QML 页面,这三种情况的使用方式都不太一样,这里总结一下。 正文 QQmlApplicationEngined搭配 Window 示例: #include <QGuiApplication> #include <QQmlApplicationEngine> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); } 这种方式是加载以 Window为跟对象的 QML 文件,QML 拥有窗口的完整控制权,可以直...
- 下一篇
Python爬取新浪微博用户信息及微博内容
大数据时代,对于研究领域来说,数据已经成为必不可少的一部分。新浪微博作为新时代火爆的新媒体社交平台,拥有许多用户行为及商户数据,因此需要研究人员都想要得到新浪微博数据,But新浪微博数据量极大,获取的最好方法无疑就是使用Python爬虫来得到。网上有一些关于使用Python爬虫来爬取新浪微博数据的教程,但是完整的介绍以及爬取用户所有数据信息比较少,因此这里分享一篇主要通过selenium包来爬取新浪微博用户数据的文章。 目标 爬取新浪微博用户数据,包括以下字段:id,昵称,粉丝数,关注数,微博数,每一篇微博的内容,转发数,评论数,点赞数,发布时间,来源,以及是原创还是转发。(本文以GUCCI(古驰)为例)方法 +使用selenium模拟爬虫 +使用BeautifulSoup解析HTML结果展示 步骤分解 1.选取爬取目标网址 首先,在准备开始爬虫之前,得想好要爬取哪个网址。新浪微博的网址分为网页端和手机端两个,大部分爬取微博数据都会选择爬取手机端,因为对比起来,手机端基本上包括了所有你要的数据,并且手机端相对于PC端是轻量级的。 下面是GUCCI的手机端和PC端的网页展示。 2.模拟登...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Hadoop3单机部署,实现最简伪集群
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果