跳至主要內容

1BRC 挑战 - IO

lament-z大约 5 分钟

1BRC 挑战 - IO

其实应该叫关于我本地磁盘的顺序读的一些测试,测试内容设计的并不是很严谨,不过能说明问题。

本文的代码在仓库open in new window的 1brc-benchmark 目录中。

准备工作

  1. 创建文件

运行 CreateFile 的 main 方法即可,一共会在 ./files 目录创建4个文件,总大小不超过 2GB。

  1. 检查 JMH 能否正常运行

你可以自己写个简单的 JMH 测试开能否正常运行,懒得写也可以去 JMH 官方提供的 Demoopen in new window 那边复制一个。

如果遇到那什么 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-6347833open in new window,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 时有挺多的坑的,注意 setuptearDown和对应的 Level,不然你的测试结果可能会很诡异。

MappedByteBuffer 的释放很麻烦,tearDown中的代码并不适用于正常情况,通常情况下释放这玩意的方式是利用反射来调用 sun.misc.Cleaner#clean 手动进行释放。

jmh

关于 jmh 也做一点简单的说明。

  • 为什么要单独创建一个项目?

    当初学的时候跟着 jmh 的文档学的,它就是这么建议的,你要测试自己代码时,可以作为依赖导入进来并测试,然后就习惯了。

  • 能不能集成/像 junit 一样使用 jmh?

    可以,不过要注意 jmh 依赖的生效阶段,这种方式下搭配 Gradle 的话最好搞个插件,虽然是社区写的但是官方有提供链接。

上次编辑于:
贡献者: Lament