使用 JMH 做 Kotlin 的基准测试

圣诞即将来临.jpg

一. 基准测试

基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

基准测试是一种测量和评估软件性能指标的活动。你可以在某个时候通过基准测试建立一个已知的性能水平(称为基准线),当系统的软硬件环境发生变化之后再进行一次基准测试以确定那些变化对性能的影响。

二. JMH

JMH(Java Microbenchmark Harness) 是专门用于进行代码的微基准测试的一套工具API,也支持基于JVM的语言例如 Scala、Groovy、Kotlin。它是由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发的工具。

三. 举例

首先,在 build.gradle 中添加 JMH 所需的依赖

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.3.10'
    id "org.jetbrains.kotlin.kapt" version "1.3.10"
}

...

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    compile "org.jetbrains.kotlin:kotlin-reflect:1.3.10"
    testCompile group: 'junit', name: 'junit', version: '4.12'

    compile "org.openjdk.jmh:jmh-core:1.21"
    kapt "org.openjdk.jmh:jmh-generator-annprocess:1.21"
    ......
}

3.1 对比 Sequence 和 List

在 Kotlin 1.2.70 的 release note 上曾说明:

使用 Sequence 有助于避免不必要的临时分配开销,并且可以显着提高复杂处理 PipeLines 的性能。

所以,有必要下面编写一个例子来证实这个说法:

import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.results.format.ResultFormatType
import org.openjdk.jmh.runner.Runner
import org.openjdk.jmh.runner.options.OptionsBuilder
import java.util.concurrent.TimeUnit

/**
 * Created by tony on 2018-12-10.
 */
@BenchmarkMode(Mode.Throughput) // 基准测试的模式,采用整体吞吐量的模式
@Warmup(iterations = 3) // 预热次数
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 测试参数,iterations = 10 表示进行10轮测试
@Threads(8) // 每个进程中的测试线程数
@Fork(2)  // 进行 fork 的次数,表示 JMH 会 fork 出两个进程来进行测试
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class SequenceBenchmark {

    @Benchmark
    fun testSequence():Int {

        return sequenceOf(1,2,3,4,5,6,7,8,9,10)
                .map{ it * 2 }
                .filter { it % 3  == 0 }
                .map{ it+1 }
                .sum()
    }

    @Benchmark
    fun testList():Int {

        return listOf(1,2,3,4,5,6,7,8,9,10)
                .map{ it * 2 }
                .filter { it % 3  == 0 }
                .map{ it+1 }
                .sum()
    }
}

fun main() {

    val options = OptionsBuilder()
            .include(SequenceBenchmark::class.java.simpleName)
            .output("benchmark_sequence.log")
            .build()
    Runner(options).run()
}

在运行上述代码之前,需要先执行 ./gradlew build

然后,再运行main函数,得到如下的结果。

# Run complete. Total time: 00:05:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                        Mode  Cnt      Score     Error   Units
SequenceBenchmark.testList      thrpt   20  15924.272 ± 305.825  ops/ms
SequenceBenchmark.testSequence  thrpt   20  23099.938 ± 515.524  ops/ms

果然,经过多次链式调用时 Sequence 比起 List 具有更高的效率。

如果把结果导出成json格式,还可以借助 jmh 相关的 gradle 插件生成可视化的报告。

fun main() {

    val options = OptionsBuilder()
            .include(SequenceBenchmark::class.java.simpleName)
            .resultFormat(ResultFormatType.JSON)
            .result("benchmark_sequence.json")
            .output("benchmark_sequence.log")
            .build()
    Runner(options).run()
}

需要依赖到这个插件:https://github.com/jzillmann/gradle-jmh-report

借助 gradle-jmh-report 生成如下的报告:

benchmark_sequence.png

3.2 内联函数和非内联函数

Kotlin 的内联函数从编译器角度将函数的函数体复制到调用处实现内联,减少了使用高阶函数带来的隐性成本。

尝试编写一个例子:

@BenchmarkMode(Mode.Throughput) // 基准测试的模式,采用整体吞吐量的模式
@Warmup(iterations = 3) // 预热次数
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 测试参数,iterations = 10 表示进行10轮测试
@Threads(8) // 每个进程中的测试线程数
@Fork(2)  // 进行 fork 的次数,表示 JMH 会 fork 出两个进程来进行测试
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class InlineBenchmark {

    fun nonInlined(block: () -> Unit) { // 不用内联的函数
        block()
    }

    inline fun inlined(block: () -> Unit) { // 使用内联的函数
        block()
    }

    @Benchmark
    fun testNonInlined() {

        nonInlined {
            println("")
        }
    }

    @Benchmark
    fun testInlined() {

        inlined {
            println("")
        }
    }

}

得到如下的结果。

# Run complete. Total time: 00:05:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                        Mode  Cnt   Score   Error   Units
InlineBenchmark.testInlined     thrpt   20  95.866 ± 4.085  ops/ms
InlineBenchmark.testNonInlined  thrpt   20  92.736 ± 3.085  ops/ms

果然,内联更高效一些。

benchmark_inline.png

3.3 协程和RxJava

自从 Kotlin 有协程这个功能之后,经常会有人提起协程和RxJava的比对。

于是,我也尝试编写一个例子,此例子使用的 Kotlin 1.3.10 ,协程的版本1.0.1,RxJava 2.2.4

@BenchmarkMode(Mode.Throughput) // 基准测试的模式,采用整体吞吐量的模式
@Warmup(iterations = 3) // 预热次数
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 测试参数,iterations = 10 表示进行10轮测试
@Threads(8) // 每个进程中的测试线程数
@Fork(2)  // 进行 fork 的次数,表示 JMH 会 fork 出两个进程来进行测试
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
@State(Scope.Thread) // 为每个线程独享
open class CoroutinesBenchmark {

    var counter1 = AtomicInteger()
    var counter2 = AtomicInteger()

    @Setup
    fun prepare() {

        counter1.set(0)
        counter2.set(0)
    }

    fun calculate(counter:AtomicInteger): Double {

        val result = ArrayList<Int>()

        for (i in 0 until 10_000) {

            result.add(counter.incrementAndGet())
        }

        return result.asSequence().filter { it % 3 ==0 }.map { it *2 + 1 }.average()
    }

    @Benchmark
    fun testCoroutines() = runBlocking {

        calculate(counter1)
    }

    @Benchmark
    fun testRxJava() = Observable.fromCallable { calculate(counter2) }.blockingFirst()

}

执行结果如下:

# Run complete. Total time: 00:05:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                            Mode  Cnt   Score   Error   Units
CoroutinesBenchmark.testCoroutines  thrpt   20  17.719 ± 2.249  ops/ms
CoroutinesBenchmark.testRxJava      thrpt   20  18.151 ± 0.429  ops/ms

此基准测试采用的是 Throughput 模式,得分越高则性能越好。从得分来看,两者差距不大。(对于两者的比较,我还没有做更多的测试。)

benchmark_coroutines.png

总结

基准测试有很多典型的应用场景,例如想比较某些方法的执行时间,对比接口不同实现在相同条件下的吞吐量等等。在这些场景下,使用 JMH 都是很不错的选择。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容