プログラムを実装する上で何通りもの書き方が存在しますが、Scalaでパフォーマンスを見たいときは、JMHを使って計測する方法が挙げられます。
マイクロベンチツールJMH
JMH
の名称は、Java Microbenchmark Harnessです。
マイクロベンチツールという名前だけに、比較的短い処理に要する時間を測るツールです。
JMHではさまざまなモードでベンチマークを実行できます
Benchmark mode | 説明 |
---|---|
Throughput | 単位時間当たりの操作数を測定。関数を実行できる回数を秒単位で表す。 |
AverageTime | 1操作あたりの平均時間 |
SampleTime | 関数の実行に要する時間(最大、最小時間など)を測定 |
SingleShotTime | 単一の関数の実行にかかる時間を測定。コールドスタート(JVMのウォームアップなし)での動作をテストするのに適している。 |
All | すべてのベンチマークモードで計測 |
※デフォルトでは、Throughput
モードで計測されます。
JMHの使い方
sbt-jmh
プラグインを使用することでsbtプロジェクトでJMHテストを実行することができます。
環境
- scala version = 2.12.4
- sbt version = 0.13.16
ドキュメントに書いてあるようにplugins.sbt
とbuild.sbt
に以下のように追記します。
- project/plugins.sbt
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.2")
- build.sbt
enablePlugins(JmhPlugin)
準備はここまでで、あとはベンチマークしたい関数に@Benchmark
のアノテーションをつけるだけです。
import org.openjdk.jmh.annotations.Benchmark
...
@Benchmark
def hoge() = {
//処理
}
※ Benchmark mode
を指定したい場合は、以下のようにモードを指定します。
import org.openjdk.jmh.annotations._
...
@Benchmark @BenchmarkMode(scala.Array(Mode.AverageTime))
def hoge() = {
//処理
}
アノテーションを書いたあとは、
プロジェクトのベースディレクトリでsbt シェルを起動させます。
$ sbt
コンパイルする。
> jmh:compile
実行することでパフォーマンスを出すことができます。
> jmh:run -i 3 -wi 3 -f1 -t1
オプション | 説明 |
---|---|
i | イテレーション (計測回数) |
wi | ウォームアップ イテレーション (ウォーミングアップ回数) |
f | フォーク |
t | スレッド |
ドキュメントより、オプションの値は以下をオススメされています。
- イテレーション(i)は、少なくとも10回以上
- ウォームアップ イテレーション(wi)は少なくとも10〜20回以上行い、それから10〜20回のイテテレーションを行うとより現実的な結果がでる。
実際にサンプルコードで試している
map,forを使った関数の比較と、flatMap、forを使った関数で比較します。
package sample
import org.openjdk.jmh.annotations._
class JMHSample {
@Benchmark
def measureMapMeth(): Seq[Int] = {
mapMeth(10000)
}
@Benchmark
def measureForMeth(): Seq[Int] = {
forMeth(10000)
}
def mapMeth(x: Int): Seq[Int] = {
1.to(x).map(x => x.*(2))
}
def forMeth(x: Int): Seq[Int] = {
for {
a <- 1 to x
} yield {
a.*(2)
}
}
@Benchmark
def measureFlatMapMeth(): Seq[Int] = {
flatMapMeth(10000)
}
@Benchmark
def measureNestForMeth(): Seq[Int] = {
nestForMeth(10000)
}
def flatMapMeth(x: Int): Seq[Int] = {
1.to(x).flatMap {
a => (1 to a).map(b => a + b)
}
}
def nestForMeth(x: Int): Seq[Int] = {
for {
a <- 1 to x
b <- 1 to a
} yield {
a + b
}
}
}
実行結果
> jmh:run -i 3 -wi 3 -f1 -t 1
・・・
[info] Benchmark Mode Cnt Score Error Units
[info] JMHSample.measureForMeth thrpt 3 9120.018 ± 15931.762 ops/s
[info] JMHSample.measureMapMeth thrpt 3 10228.366 ± 3842.323 ops/s
[info] JMHSample.measureNestForMeth thrpt 3 0.135 ± 0.930 ops/s
[info] JMHSample.measureFlatMapMeth thrpt 3 0.163 ± 1.283 ops/s
[success] Total time: 140 s
ウォームアップ、イテレーションの回数を増やしもう一度行って見ます。
> jmh:run -i 20 -wi 20 -f1 -t 1
・・・
[info] Benchmark Mode Cnt Score Error Units
[info] JMHSample.measureForMeth thrpt 20 10260.024 ± 1172.508 ops/s
[info] JMHSample.measureMapMeth thrpt 20 10383.953 ± 843.856 ops/s
[info] JMHSample.measureNestForMeth thrpt 20 0.175 ± 0.023 ops/s
[info] JMHSample.measureFlatMapMeth thrpt 20 0.191 ± 0.022 ops/s
[success] Total time: 567 s
Errorが減ってScoreも上がっていますが、ほとんど差がないですね。
数値だけ見ると1秒間あたりの処理回数は、for
を使っている関するよりもmap
を使っている関数の方が高いですが、
何度か同じ条件で実行すると、Errorが多かった方のScoreが下がっていましたので
一概にどっちがいいのかは言えなさそうです。
終わりに
JMHを使って簡単に1秒間あたりの処理回数が調べられて大変便利だなと感じました。
Socoreに関しては明らかな差がある場合は有効かと思いましたが、今回のようにあまり差がない場合は、何度か繰り返すと結果が変わるので、同じ性能くらいの認識で良いのかなと思いまます。
今回は簡単なコードだったのですが、実際にもっと処理が多い関数などで調査したい場合には良いのではないかと思います。
参考記事