CLOVER🍀

That was when it all began.

Scala+sbtと、JMHを合わせて使う

2014/7/6追記)
sbt-jmhというsbt用のJMHプラグインができたそうなので、そちら向けのエントリを書きました。

sbt-jmhを使って、ScalaでマイクロベンチマークComments
http://d.hatena.ne.jp/Kazuhira/20140706/1404641244

先ほど、こんなエントリを投稿しました。

マイクロベンチマークツール、JMHを試す
http://d.hatena.ne.jp/Kazuhira/20140102/1388662362

この時、

$ mvn package

でJARファイルを作って実行するんだよ、みたいな内容を書きましたが、このJARの中身をよくよく見ると

$ jar -tvf target/microbenchmarks.jar 
   167 Thu Jan 02 20:21:18 JST 2014 META-INF/MANIFEST.MF
     0 Thu Jan 02 20:21:18 JST 2014 META-INF/
   585 Thu Jan 02 20:21:18 JST 2014 META-INF/MicroBenchmarks
   177 Thu Jan 02 20:21:18 JST 2014 META-INF/CompilerHints
     0 Thu Jan 02 20:21:18 JST 2014 org/
     0 Thu Jan 02 20:21:18 JST 2014 org/littlewings/
     0 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/
     0 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/
  1354 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/TestState.class
     0 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/
   531 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$TestState_jmh.class
  7040 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$BlackHole_jmh_B1.class
 19406 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState.class
  7010 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$Loop_jmh_B1.class
  1403 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$Loop_jmh_B2.class
  7104 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$BlackHole_jmh_B3.class
  7042 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$HelloJmh_jmh_B1.class
   521 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$HelloJmh_jmh.class
  1448 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$BlackHole_jmh_B2.class
  7099 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$BlackHole_jmh_B3.class
   481 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$Loop_jmh.class
  1453 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$TestState_jmh_B2.class
  7054 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$Loop_jmh_B3.class
   506 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$BlackHole_jmh.class
 18988 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop.class
  7104 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$TestState_jmh_B3.class
  7079 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$BlackHole_jmh_B3.class
   531 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$BlackHole_jmh.class
  7094 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$HelloJmh_jmh_B3.class
   526 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$BlackHole_jmh.class
  7044 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$BlackHole_jmh_B1.class
 15848 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh.class
  7024 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$BlackHole_jmh_B1.class
  7050 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$TestState_jmh_B1.class
  1453 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/TestState$BlackHole_jmh_B2.class
  1428 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/Loop$BlackHole_jmh_B2.class
  1443 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/generated/HelloJmh$HelloJmh_jmh_B2.class
   839 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/Loop.class
   576 Thu Jan 02 20:21:18 JST 2014 org/littlewings/jmh/example/HelloJmh.class

〜以降JMHやargs4jのクラスが続く〜

となっていて、作成したJavaのクラスは3つだったのに、大量のクラスが作成されています。

つまり、Pluggable Annotation Processing APIが使われてコンパイル時にクラスが生成されているわけですね…。

ここでふと思う。Scalaで使えない?

たぶんダメだろうと思いつつ、こんなsbtの定義と
build.sbt

import AssemblyKeys._

name := "jmh-example"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation")

libraryDependencies += "org.openjdk.jmh" % "jmh-core" % "0.2"

assemblySettings

jarName in assembly := "microbenchmarks.jar"

test in assembly := {}

mainClass in assembly := Some("org.openjdk.jmh.Main")

sbt-assemblyプラグインの設定、
project/assembly.sbt

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.10.1")

そして、こんなScalaコードを用意します。
src/main/scala/org/littlewings/jmh/example/HelloJmhScala.scala

package org.littlewings.jmh.example

import java.util.concurrent.TimeUnit

import org.openjdk.jmh.annotations.GenerateMicroBenchmark

class HelloJmhScala {
  @GenerateMicroBenchmark
  def simpleBench(): Unit =
    TimeUnit.MILLISECONDS.sleep(500L)
}

で、JARファイルを作って…

> assembly
[info] Updating {file:/xxxxx/}jmh-example...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/classes...
[info] Including: jmh-core-0.2.jar
[info] Including: args4j-2.0.16.jar
[info] Including: scala-library.jar
[info] Checking every *.class/*.jar file's SHA-1.
[info] Merging files...
[warn] Merging 'LICENSE' with strategy 'rename'
[warn] Merging 'META-INF/MANIFEST.MF' with strategy 'discard'
[warn] Strategy 'discard' was applied to a file
[warn] Strategy 'rename' was applied to a file
[info] SHA-1: a26154cdfcae13645ae2bac5f365a1d85df412cf
[info] Packaging /xxxxx/target/scala-2.10/microbenchmarks.jar ...
[info] Done packaging.
[success] Total time: 13 s, completed 2014/01/02 21:56:57

sbtを立ち上げているターミナルとは、別のターミナルから実行。

$ java -jar target/scala-2.10/microbenchmarks.jar 
No matching benchmarks. Miss-spelled regexp? Use -v for verbose output.

案の定、ベンチマークがないんだけど?と言われます。

本来は「-l」オプションでベンチマークの一覧が見れるのですが、何も出てきません。

$ java -jar target/scala-2.10/microbenchmarks.jar -l
Benchmarks: 

まあ、仕方がないですね。

ちなみに、sbt-assemblyプラグインは初めて使いました。

と、ここで諦めるのもなんなので、普通にJavaで被せました。以降、先のエントリの内容をJava+Scalaで淡々と移植していくので、JMH自体に興味のある方は前のエントリを参照してください。

HelloWorld的なサンプルの移植

計測対象。
src/main/scala/org/littlewings/jmh/example/HelloJmhTarget.scala

package org.littlewings.jmh.example

import java.util.concurrent.TimeUnit

object HelloJmhTarget {
  def invoke(): Unit = TimeUnit.MILLISECONDS.sleep(500L)
}

ベンチマーク側。
src/main/java/org/littlewings/jmh/example/HelloJmh.java

package org.littlewings.jmh.example;

import org.openjdk.jmh.annotations.GenerateMicroBenchmark;

public class HelloJmh {
    @GenerateMicroBenchmark
    public void simpleBench() throws InterruptedException {
        HelloJmhTarget.invoke();
    }
}

これで、普通に実行できるようになります。

まずはassembly。

> assembly
[info] Updating {file:/xxxxx/}jmh-example...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 4 Scala sources and 3 Java sources to /xxxxx/target/scala-2.10/classes...
[warn] 警告: 注釈プロセッサ'org.openjdk.jmh.processor.internal.GenerateMicroBenchmarkProcessor'から-source '1.7'より小さいソース・バージョン'RELEASE_6'がサポートされています
[warn] 警告: 注釈プロセッサ'org.openjdk.jmh.processor.internal.HelperMethodValidationProcessor'から-source '1.7'より小さいソース・バージョン'RELEASE_6'がサポートされています
[warn] 警告: 注釈プロセッサ'org.openjdk.jmh.processor.internal.GroupValidationProcessor'から-source '1.7'より小さいソース・バージョン'RELEASE_6'がサポートされています
[warn] 警告: 注釈プロセッサ'org.openjdk.jmh.processor.internal.CompilerControlProcessor'から-source '1.7'より小さいソース・バージョン'RELEASE_6'がサポートされています
[warn] 警告4個
[info] Including: jmh-core-0.2.jar
[info] Including: args4j-2.0.16.jar
[info] Including: scala-library.jar
[info] Checking every *.class/*.jar file's SHA-1.
[info] Merging files...
[warn] Merging 'LICENSE' with strategy 'rename'
[warn] Merging 'META-INF/MANIFEST.MF' with strategy 'discard'
[warn] Strategy 'discard' was applied to a file
[warn] Strategy 'rename' was applied to a file
[info] SHA-1: 4f5bb4dfa6ff9d99dc38d1971cb2a4234d4eaa7e
[info] Packaging /xxxxx/target/scala-2.10/microbenchmarks.jar ...
[info] Done packaging.
[success] Total time: 10 s, completed 2014/01/02 22:44:46

JMHがサポートしているAnnotation Processorが、Java 6なんだけどと言われていますが、とりあえず気にしない…。

生成されたJARファイルを実行。

$ java -jar target/scala-2.10/microbenchmarks.jar -wi 3 -i 3 -f 1 -bm thrpt -tu s

結果。

Benchmark                        Mode Thr     Count  Sec         Mean   Mean error    Units
o.l.j.e.HelloJmh.simpleBench    thrpt   1         3    1        2.000        0.000    ops/s

まあ、当たり前と言えば当たり前ですが…。

繰り返しと状態保持のサンプル移植

あとは、もう適当に移植…。

繰り返しのScala側。
src/main/scala/org/littlewings/jmh/example/LoopTarget.scala

package org.littlewings.jmh.example

import java.util.concurrent.TimeUnit

object LoopTarget {
  def method(limit: Int): Unit =
    (0 until limit).foreach(i => TimeUnit.MILLISECONDS.sleep(10))
}

ベンチマークのJava側。
src/main/java/org/littlewings/jmh/example/Loop.java

package org.littlewings.jmh.example;

import org.openjdk.jmh.annotations.GenerateMicroBenchmark;
import org.openjdk.jmh.annotations.OperationsPerInvocation;

public class Loop {
    @GenerateMicroBenchmark
    @OperationsPerInvocation(1)
    public void invoke1() throws InterruptedException {
        LoopTarget.method(1);
    }

    @GenerateMicroBenchmark
    @OperationsPerInvocation(100)
    public void invoke100() throws InterruptedException {
        LoopTarget.method(100);
    }
}

状態保持のサンプルの移植。

Scala側。
src/main/scala/org/littlewings/jmh/example/TestStateTarget.scala

package org.littlewings.jmh.example

import scala.beans.BeanProperty

class TestStateTarget {
  @BeanProperty
  var counter: Long = _

  def methodA(): Unit =
    counter += 1

  def methodB(): Unit =
    counter += 1
}

ベンチマークのJava側。
src/main/java/org/littlewings/jmh/example/TestState.java

package org.littlewings.jmh.example;

import org.openjdk.jmh.annotations.GenerateMicroBenchmark;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;

@State(Scope.Thread)
public class TestState {
    private TestStateTarget target = new TestStateTarget();

    @Setup
    public void setup() {
        System.out.println("========== setup ==========");
        target.setCounter(0);
    }

    @GenerateMicroBenchmark
    public void methodA() {
        target.methodA();
    }

    @GenerateMicroBenchmark
    public void methodB() {
        target.methodB();
    }

    @TearDown
    public void tearDown() {
        System.out.println("========== teardown ==========");
        System.out.println("counter = " + target.getCounter());
    }
}

実行!

$ java -jar target/scala-2.10/microbenchmarks.jar -wi 3 -i 3 -f 1 -bm thrpt -tu s

結果。

Benchmark                        Mode Thr     Count  Sec         Mean   Mean error    Units
o.l.j.e.HelloJmh.simpleBench    thrpt   1         3    1        2.000        0.000    ops/s
o.l.j.e.Loop.invoke1            thrpt   1         3    1       89.689        7.197    ops/s
o.l.j.e.Loop.invoke100          thrpt   1         3    1       91.989       10.924    ops/s
o.l.j.e.TestState.methodA       thrpt   1         3    1 401082658.122 83017505.408    ops/s
o.l.j.e.TestState.methodB       thrpt   1         3    1 354834340.622 199448544.886    ops/s

そんなにJavaと変わらなそうだから、大丈夫かな…。

とりあえず、sbt-assemblyプラグインと最低限のJavaのブリッジコードを書けば、Scala+sbtでもJMHは使えそうだということで。