JMHを使ってScalaのパフォーマンスを計測する

プログラムを実装する上で何通りもの書き方が存在しますが、Scalaでパフォーマンスを見たいときは、JMHを使って計測する方法が挙げられます。

# マイクロベンチツールJMH
`JMH`の名称は、[Java Microbenchmark Harness](http://openjdk.java.net/projects/code-tools/jmh/)です。
マイクロベンチツールという名前だけに、比較的短い処理に要する時間を測るツールです。

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


[ドキュメント](https://github.com/ktoso/sbt-jmh/blob/master/README.md)に書いてあるように`plugins.sbt`と`build.sbt`に以下のように追記します。

- project/plugins.sbt

```scala
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.2")
```

- build.sbt

```scala
enablePlugins(JmhPlugin)
```

準備はここまでで、あとはベンチマークしたい関数に`@Benchmark`のアノテーションをつけるだけです。

```Scala
import org.openjdk.jmh.annotations.Benchmark
...
 @Benchmark
 def hoge() = {
//処理
}
```
※ `Benchmark mode`を指定したい場合は、以下のようにモードを指定します。

```Scala
import org.openjdk.jmh.annotations._
...
 @Benchmark @BenchmarkMode(scala.Array(Mode.AverageTime))
 def hoge() = {
//処理
}
```
アノテーションを書いたあとは、
プロジェクトのベースディレクトリでsbt シェルを起動させます。

```shell
$ sbt 
```

コンパイルする。

```scala
> jmh:compile
```

実行することでパフォーマンスを出すことができます。

```scala
> jmh:run -i 3 -wi 3 -f1 -t1
```

| オプション |    説明 |
|:-----------------|:-----------------|
|i |  イテレーション (計測回数) |
|wi |  ウォームアップ イテレーション (ウォーミングアップ回数) |
|f|フォーク|
|t|スレッド|

[ドキュメント](https://github.com/ktoso/sbt-jmh/blob/master/README.md#adding-to-your-project)より、オプションの値は以下をオススメされています。

- イテレーション(i)は、少なくとも10回以上
- ウォームアップ イテレーション(wi)は少なくとも10〜20回以上行い、それから10〜20回のイテテレーションを行うとより現実的な結果がでる。

# 実際にサンプルコードで試している

map,forを使った関数の比較と、flatMap、forを使った関数で比較します。

```scala
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
    }
  }
}
```

実行結果

```shell
> 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
```

ウォームアップ、イテレーションの回数を増やしもう一度行って見ます。

```shell
> 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に関しては明らかな差がある場合は有効かと思いましたが、今回のようにあまり差がない場合は、何度か繰り返すと結果が変わるので、同じ性能くらいの認識で良いのかなと思いまます。

今回は簡単なコードだったのですが、実際にもっと処理が多い関数などで調査したい場合には良いのではないかと思います。

参考記事

- [マイクロベンチマークツールのsbt-jmhを使ってみた](http://labs.septeni.co.jp/entry/2015/08/09/045258)
- [JMH - Java Microbenchmark Harness](http://tutorials.jenkov.com/java-performance/jmh.html)
- [ソフトウェアエンジニアリング](http://www.torutk.com/projects/swe/wiki/JMH)
- [性能の測定](https://docs.scala-lang.org/ja/overviews/parallel-collections/performance.html)
- [Microbenchmarking Scala with JMH](https://chariotsolutions.com/blog/post/microbenchmarking-scala-with-jmh/)