1BRC 挑战 - IO
1BRC 挑战 - IO
其实应该叫关于我本地磁盘的顺序读的一些测试,测试内容设计的并不是很严谨,不过能说明问题。
本文的代码在仓库的 1brc-benchmark 目录中。
准备工作
- 创建文件
运行 CreateFile
的 main 方法即可,一共会在 ./files
目录创建4个文件,总大小不超过 2GB。
- 检查 JMH 能否正常运行
你可以自己写个简单的 JMH 测试开能否正常运行,懒得写也可以去 JMH 官方提供的 Demo 那边复制一个。
如果遇到那什么 XXList not found
还是啥的错误,说明 jmh 的注解没有正常运行,因此没能自动生成那个文件,你要做的就是翻翻你的 ide 的设置里面,找到 Annotation processor
或者 compiler
,找到并启用编译时的注解处理。
如果依然没有解决问题,请把 ide 中你自己指定的 Gradle 切换为 ide 默认的。
测试简介
运行测试的方法也很简单,就是执行每个文件的 main 方法即可,无需安装相关插件。
ReadSpeedBenchmark
对比 MappedByteBuffer 和 FileChannel 顺序读取速度,目标文件大小 1GB,一次读 4kb。
FileChannelRead_4k 就是每次从文件中读 4kb;
mappedFileRead_4k 是直接映射完整文件,并且每次测试时会先 load,之后每次从 buffer 中读取 4KB;
mappedFileRead_WithLoad_4k 也是直接映射完整文件,但不提前 load,也是每次读 4kb。
result
Benchmark Mode Cnt Score Error Units ReadSpeedBenchmark.FileChannelRead_4k avgt 5 822.790 ± 75.768 ms/op ReadSpeedBenchmark.mappedFileRead_4k avgt 5 481.891 ± 36.424 ms/op ReadSpeedBenchmark.mappedFileRead_WithLoad_4k avgt 5 456.414 ± 32.878 ms/op
可以看到
FileChannelRead_4k
很慢,这是因为我们的数据块太小了,才 4kb,26 万多次才能读完,与下面相比能明显看到频繁切换 context 和多次拷贝带来的消耗,后续的测试中这一点更明显。而 MappedByteBuffer 虽然从结果上看似乎提前 load 并没有带来优势,但这个并不是绝对的,哪怕在我的机器上反复运行该测试,这俩耗时可能都会反过来。
不过这里测试的重点不是
MappedByteBuffer
提前 load 是否有优势,而是在我机器上,在 1BRC 场景下,二者没啥太大区别。
MBBReadDiffSizeBenchmark
这个测试就是简单的测试一下 MappedByteBuffer 读取不同大小文件时的速度。
这个测试明显偷懒了,被测试文件大小的跨度有点大,有兴趣可以自行添加更多文件大小。
result
MBBReadDiffSizeBenchmark.mfRead_128MB avgt 3 63.555 ± 28.550 ms/op MBBReadDiffSizeBenchmark.mfRead_1MB avgt 3 0.582 ± 0.088 ms/op MBBReadDiffSizeBenchmark.mfRead_512MB avgt 3 255.717 ± 33.856 ms/op
为了方便比较,我们对结果进行一下转换。
1MB 的读取速度 1.72 MB/ms | 1/0.58;
128MB 的读取速度 2.03 MB/ms | 128/63;
512MB 的读取速度 2.00 MB/ms | 512/255;
可以看到这就是很多地方说的 MappedByteBuffer 适合大文件的读写,当然我们这里只测试了读,而且设计的不太科学。
顺便说一句,MappedByteBuffer 这个所谓的“适合大文件”的读写其实也并不绝对,一定要根据实际情况去做验证测试,而且它有
Integer.MaxValue
这个限制,这个限制存在已久,就比如JDK-6347833,Java 并不打算去改他,1BRC 这个文件才 13GB,按 8 核分一下每个核也就处理 1.6GB,再大就要用别的了。
FileChannelReadBenchmark
目标文件 1GB,以不同数据块大小进行读取。
嫌测试慢可以把 32 KB 以下的方法都注释掉。
result
该结果仅供参考,不同情况下测试结果会有出入。
Benchmark Mode Cnt Score Error Units FileChannelReadBenchmark.FileChannelRead_1kb avgt 3 2868.184 ± 3317.082 ms/op FileChannelReadBenchmark.FileChannelRead_1mb avgt 3 245.871 ± 149.714 ms/op FileChannelReadBenchmark.FileChannelRead_2kb avgt 3 1373.387 ± 165.363 ms/op FileChannelReadBenchmark.FileChannelRead_2mb avgt 3 257.460 ± 240.522 ms/op FileChannelReadBenchmark.FileChannelRead_32kb avgt 3 274.294 ± 227.031 ms/op FileChannelReadBenchmark.FileChannelRead_32mb avgt 3 311.100 ± 71.928 ms/op FileChannelReadBenchmark.FileChannelRead_4kb avgt 3 802.166 ± 716.383 ms/op FileChannelReadBenchmark.FileChannelRead_4mb avgt 3 276.631 ± 300.579 ms/op FileChannelReadBenchmark.FileChannelRead_64kb avgt 3 243.696 ± 23.903 ms/op FileChannelReadBenchmark.FileChannelRead_64mb avgt 3 390.312 ± 322.808 ms/op FileChannelReadBenchmark.FileChannelRead_odd1 avgt 3 248.613 ± 189.748 ms/op FileChannelReadBenchmark.FileChannelRead_odd2 avgt 3 244.240 ± 72.118 ms/op
odd1 = 1mb - 1b | odd2 = 1mb - 4k
在我机器上 32kb ~ 32mb 这个区间读取速度都很不错,奇数块会带来一定的性能损失,但是正常情况下影响不大。
MappedBufferReadBenchmark
同前一个测试,只不过换成 MappedByteBuffer
。
目标文件依然是直接映射,没有提前 load,以不同大小 buffer 从 MappedByteBuffer 中读取。
其实这个测试意义不大,毕竟只要循环次数不要出现数量级的差距(比如一次只读 1b),那么测试结果应该都是一个均值,根据第一个测试的结果来看,应该就是 500ms 上下波动。
result
Benchmark Mode Cnt Score Error Units MappedBufferReadBenchmark.mfRead_128mb avgt 3 569.466 ± 66.756 ms/op MappedBufferReadBenchmark.mfRead_1kb avgt 3 480.408 ± 156.885 ms/op MappedBufferReadBenchmark.mfRead_1mb avgt 3 499.358 ± 32.810 ms/op MappedBufferReadBenchmark.mfRead_2kb avgt 3 479.836 ± 273.726 ms/op MappedBufferReadBenchmark.mfRead_2mb avgt 3 524.473 ± 141.532 ms/op MappedBufferReadBenchmark.mfRead_32kb avgt 3 495.311 ± 440.596 ms/op MappedBufferReadBenchmark.mfRead_32mb avgt 3 566.165 ± 156.340 ms/op MappedBufferReadBenchmark.mfRead_4kb avgt 3 507.535 ± 559.356 ms/op MappedBufferReadBenchmark.mfRead_4mb avgt 3 523.195 ± 207.555 ms/op MappedBufferReadBenchmark.mfRead_512mb avgt 3 569.649 ± 58.999 ms/op MappedBufferReadBenchmark.mfRead_64kb avgt 3 492.300 ± 217.908 ms/op MappedBufferReadBenchmark.mfRead_64mb avgt 3 601.457 ± 465.398 ms/op MappedBufferReadBenchmark.mfRead_odd1 avgt 3 520.109 ± 252.120 ms/op MappedBufferReadBenchmark.mfRead_odd2 avgt 3 517.124 ± 751.281 ms/op
从结果可以看到基本符合预期,如果后台程序全部关关掉,然后打开 mac 的高性能模式的话,这些数据应该会整体快一丢丢,并且误差值没那么大。
可以看到
MappedByteBuffer
不会有FileChannel
那种波动性,它的波动来自于自动载入数据(本质就是写缓存页),不严谨的说就是页错误次数的多少。不过至此可以得出一个结论,在我的机器上,只要数据块控制在 32MB 左右,用
FileChannel
快大概 30%。
补充说明
由于不相关,所以没有列出 MappedByteBuffer
已加载后的读取速度测试,那个相当快了,基本都是 100ms 以内。
使用 jmh 测试 MappedByteBuffer
时有挺多的坑的,注意 setup
、tearDown
和对应的 Level
,不然你的测试结果可能会很诡异。
MappedByteBuffer
的释放很麻烦,tearDown
中的代码并不适用于正常情况,通常情况下释放这玩意的方式是利用反射来调用 sun.misc.Cleaner#clean
手动进行释放。
jmh
关于 jmh 也做一点简单的说明。
为什么要单独创建一个项目?
当初学的时候跟着 jmh 的文档学的,它就是这么建议的,你要测试自己代码时,可以作为依赖导入进来并测试,然后就习惯了。
能不能集成/像 junit 一样使用 jmh?
可以,不过要注意 jmh 依赖的生效阶段,这种方式下搭配 Gradle 的话最好搞个插件,虽然是社区写的但是官方有提供链接。