使用 JMH 做 Kotlin 的基準測試

一. 基準測試

基準測試是指通過設計科學的測試方法、測試工具和測試系統,實現對一類測試對象的某項性能指標進行定量的和可對比的測試。

基準測試是一種測量和評估軟件性能指標的活動。你可以在某個時候通過基準測試建立一個已知的性能水平(稱爲基準線),當系統的軟硬件環境發生變化之後再進行一次基準測試以確定那些變化對性能的影響。

二. 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 生成如下的報告:

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

果然,內聯更高效一些。

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 模式,得分越高則性能越好。從得分來看,兩者差距不大。(對於兩者的比較,我還沒有做更多的測試。)

總結

基準測試有很多典型的應用場景,例如想比較某些方法的執行時間,對比接口不同實現在相同條件下的吞吐量等等。在這些場景下,使用 JMH 都是很不錯的選擇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章