JMH(Java Microbenchmark Harness)のサンプルを動かしながら勉強

先月末に開催されたSpringOne Platform 2018の録画がもうアップされてて、ちょこっとずつ見てってるんだけど、何日か前にDaveのセッションを見た。面白かった。

How Fast is Spring? - YouTube

Springアプリの起動スピードについてのセッション。で、その起動スピードを自分でも確認したいなーと思ったんだけど、DaveがベンチマークのツールとしてOpenJDKのJMHというものを使ってるので、まずはそれを知らなきゃなと思って触ってみた。

OpenJDK: jmh

## Hello JMH

JMHのサイトに書いてある通り、プロジェクトを作って

$ mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=com.example \
          -DartifactId=hello-jmh \
          -Dversion=1.0

こうやったら実行できる。

$ cd hello-jmh
$ mvn clean install
$ java -jar target/benchmarks.jar

(...)

Result "com.example.MyBenchmark.testMethod":
  2431425768.902 ±(99.9%) 262046906.678 ops/s [Average]
  (min, avg, max) = (1148300545.795, 2431425768.902, 2787377007.996), stdev = 349825123.190
  CI (99.9%): [2169378862.223, 2693472675.580] (assumes normal distribution)

空っぽのメソッドのベンチマークを、デフォルトの設定で実行して取得してるみたいね。

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.
    }

## そもそもJMHって?

マイクロベンチマークは色んなものが影響するから難しいみたい。へー。そうなのかー。

  • ウォームアップ
    • 一定回数以上呼び出されたらJIT(Just-in-Time)コンパイラーがバイトコードをマシンコードに変換するから、それを考慮したウォームアップが必要
  • GC(Garbage Collection)の影響
  • JITコンパイラーのDead-Code Eliminationなどの最適化の影響
  • などなど

なので、その辺りを考慮して作られてるのがJMH。

  • ウォームアップ機能
  • リフレクションじゃなくてAnnotation Processingを使ってコンパイル時にコードを生成
  • 対象のコードをラッピングして、ループの最適化が発生しない繰り返し回数を設定
  • Dead-Code Eliminationを回避するための仕組み
  • などなど

## 具体的にどう使うんだろう?

と思ったので、この本のCh.5をさらーっとだけ読んで雰囲気だけ理解してから

www.safaribooksonline.com

サンプルコードを落としてきて読んだ。

http://hg.openjdk.java.net/code-tools/jmh/file/66fb723292d4/jmh-samples/src/main/java/org/openjdk/jmh/samples

各クラスのmain()メソッドのところのコメントに起動コマンド書いてるから、時間短縮のためにそこにちょこっとパラメーターを足して実行したりした。

## 1. ベンチマークの対象について

### @Benchmark

    @Benchmark
    public void wellHelloThere() {
  • @Benchmarkをつけたメソッドがベンチマークの対象になる
  • publicなメソッドじゃないといけない
  • 引数に許されているのは@StateアノテーションのついたクラスかJMH infrastructureのクラス(あとで出てくるBlackholeとか)のみ

### @BenchmarkMode

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void measureThroughput() throws InterruptedException {
  • @Benchmarkメソッドにつけてモードを指定
  • クラスにつけることもできて、その場合はクラス内の全部の@Benchmarkに反映される
  • 実行時のオプションで上書きすることができる
  • ↓こんな風に複数のモードを指定することも可能
    @Benchmark
    @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime})
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void measureMultiple() throws InterruptedException {

### モード

  • Mode.Throughput
    • 決められた時間の中で実行を繰り返して
    • 単位時間あたりに何回実行できたかを計測
  • Mode.AverageTime
    • 決められた時間の中で実行を繰り返して
    • 平均実行時間を計測
  • Mode.SampleTime
    • 決められた時間の中で実行を繰り返すが
    • その中のいくつかのサンプルで実行時間を計測
  • Mode.SingleShotTime
    • 1回だけ実行してその時間を計測
  • Mode.All
    • 上記の全てのモードを指定したのと同じ

### @OutputTimeUnit

  • 単位時間を指定
  • メソッド、クラスのどちらにでも指定可能
  • 実行時オプションで上書き可能

## 2. ベンチマークの状態について

### @State

  • クラスにつける
  • ベンチマークが実行されている状態をカプセル化
  • 通常は@Benchmarkメソッドに引数として渡される
  • その初期化と共有はJMHが面倒をみる
    @State(Scope.Benchmark)
    public static class BenchmarkState {
        volatile double x = Math.PI;
    }

    @State(Scope.Thread)
    public static class ThreadState {
        volatile double x = Math.PI;
    }

    @Benchmark
    public void measureUnshared(ThreadState state) {
        state.x++;
    }

    @Benchmark
    public void measureShared(BenchmarkState state) {
        state.x++;
    }

### @Stateのスコープ

  • Scope.Benchmark
    • 全てのワーカースレッドで共有される
  • Scope.Thread
  • Scope.Group
    • スレッドグループ内で共有される

### Default State

@State(Scope.Thread)
public class JMHSample_04_DefaultState {

    double x = Math.PI;

    @Benchmark
    public void measure() {
        x++;
    }

### State Fixtures

  • @Setup@TearDownをメソッドにつけることで、Stateの前処理や後処理をすることができる
  • FixtureメソッドにはLevelを指定することができる

## 3. Blackholeについて

### Dead-Code Elimination

  • ベンチマーク対象のコードが、コンパイラーのDead-Code Elimination対象にならないようにする
  • メソッドから結果を返すことでJMHのBlackholeがそれを使うので削除対象外になる
    private double x = Math.PI;

    @Benchmark
    public void measureWrong() {
        // これだと結果が利用されないので削除対象になる
        Math.log(x);
    }

    @Benchmark
    public double measureRight() {
        // これで結果が利用されるので大丈夫
        return Math.log(x);
    }

結果はこうなった。baselineは何もしてないメソッド。面白い。

Benchmark                           Mode  Cnt   Score   Error  Units                                                                                                                                   
JMHSample_08_DeadCode.baseline      avgt    5   0.357 ± 0.033  ns/op                                                                                                                                   
JMHSample_08_DeadCode.measureRight  avgt    5  25.322 ± 0.873  ns/op                                                                                                                                   
JMHSample_08_DeadCode.measureWrong  avgt    5   0.352 ± 0.010  ns/op

### 複数対応

これだとMath.log(x1);の方は削除対象になる

    @Benchmark
    public double measureWrong() {
        Math.log(x1);
        return Math.log(x2);
    }

のでこうするか

    @Benchmark
    public double measureRight_1() {
        return Math.log(x1) + Math.log(x2);
    }

Blackholeを引数に渡してこうする

    @Benchmark
    public void measureRight_2(Blackhole bh) {
        bh.consume(Math.log(x1));
        bh.consume(Math.log(x2));
    }

### Constant Folding

  • 結果が同じ値になることが分かってる場合にConstant Foldingで最適化されて畳み込まれてしまうことを防ぐ
  • そのためには@Stateの非finalフィールドを使用する
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_10_ConstantFold {

    private double x = Math.PI;

    private final double wrongX = Math.PI;

    @Benchmark
    public double measureWrong_1() {
        // これだと畳み込めるからダメ。
        return Math.log(Math.PI);
    }

    @Benchmark
    public double measureWrong_2() {
        // これも畳み込めるからダメ。
        return Math.log(wrongX);
    }

    @Benchmark
    public double measureRight() {
        // これだと予測可能にならないからOK
        return Math.log(x);
    }

結果はこうなった。面白いー。

Benchmark                                 Mode  Cnt   Score   Error  Units
JMHSample_10_ConstantFold.baseline        avgt    5   2.693 ± 1.774  ns/op
JMHSample_10_ConstantFold.measureRight    avgt    5  25.476 ± 1.170  ns/op
JMHSample_10_ConstantFold.measureWrong_1  avgt    5   2.662 ± 1.867  ns/op
JMHSample_10_ConstantFold.measureWrong_2  avgt    5   2.684 ± 1.870  ns/op

ここまででサンプルの1から10までをざっくりカバーしたくらいかな。サンプルは38まであるから、また気が向いたときに続きやるかもー。

「ちゃんとやってるのになんかうまくいかないスクラムからの脱出」ってタイトルでプロポーザルを出しました!

聞いてみたいなと思ってくれた方は、ハートマークのいいねの投票をポチッとお願いしますー。

confengine.com

REGIONAL SCRUM GATHERING® TOKYO 2019です!

## REGIONAL SCRUM GATHERING® TOKYO 2019

2019.scrumgatheringtokyo.org

年に一回色んなところから人が集まってきて、スクラムを中心にした知識や体験を交換できるとこです。楽しいやつ。

## 改善グループ

僕は楽天でWebアプリケーションエンジニアとして働いています。数年前に「エンジニアとして色んなチームを内側からサポートしたい!」と考えて「改善グループ」というグループを立ち上げました。そして、様々なチームの中に入っていって、一緒にコードを書いたり、テストを書いたりして楽しんでいます。

## 土台としての開発プロセス

そんな風にエンジニアとして動くことを中心にしてはいるのですが、実は毎回最初にやるのはチームの開発プロセスを一緒に改善することなんです。というのも、エンジニアとして動くための土台としての開発プロセスが、とても重要だからです。

## 「スクラムをやってはいるのだけど、何かうまくいかない」

僕の所属しているECインキュベーション開発部では、どのチームもスクラムで開発をしているのですが、この活動の中でよく聞いたのは「スクラムをやってはいるのだけど、何かうまくいかない」ということです。

## 実体験を紹介したいなと思います

このセッションでは「スクラムが何かうまくいかない」と感じているチームに実際にどのような課題があったのか、その原因は何だったのか、そしてそれをどのように改善したのか、をお伝えしたいなと思います。

この辺の話とか

bufferings.hatenablog.com

あとは、うまくいかなかった話も交えながら。

## 一歩踏み出すきっかけになるといいな

同じような課題を感じている方が一歩踏み出すきっかけになるといいなと思います。そして、セッションの後にはみなさんと「うちもこういうことがあるよー・あったよー」みたいな話をできたら嬉しいです!

## 投票お願いしますー!

ので、聞いてみたいなと思ってくれた方は、ハートマークのいいねの投票をポチッとお願いしますー。他にも色んな面白そうなセッションがあるから、色々見てみて興味あるやつをポチッと!

JDK 11の重要なポイントを勉強してきた

ちょうど今週は東京に出張で来てたのでJJUGナイトセミナーに参加してきたー。面白かった!

jjug.doorkeeper.jp

Java is Still Free

会場に着いたらJava is Still Freeの日本語版がスクリーンに映ってた。じゅくちょーすごいなぁ。

by 伊藤 敬さん

JDK 11リリースなので改めて「新しいJDKリリースモデル解説(サマリー&アップデート)」 by 伊藤 敬さん

公式情報のおさらい!

by 久保田 祐史さん

Java 11 : サポートとVM機能 編 by 久保田 祐史さん

分かりやすかったー。

by きしだなおきさん

Java 11:APIの変更点 編 by きしだなおきさん

JEPの話と、JEP以外の変更の話とで面白かったー。会場到着が間に合ってよかったね。

Stringの+=に副作用があるバグ

のお話をきしださんがしてて面白そうだったので試してみた。

❯ docker run --rm -ti openjdk:10.0.1-jdk jshell
Oct 09, 2018 3:10:52 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.1
|  For an introduction type: /help intro

jshell> var s = new String[]{"1", "2", "3"};
s ==> String[3] { "1", "2", "3" }

jshell> var i = 0;
i ==> 0

jshell> s[i++] += "a";
$3 ==> "2a"

jshell> i
i ==> 2

jshell> s
s ==> String[3] { "2a", "2", "3" }

おー!i1じゃなくて2になってるし、1aじゃなくて2aになってる!バグってる!

詳しくはこちら:

Java9、10でStringの+=に副作用があるバグ - きしだのはてな

2019-05-01以降でHEISEIが使えない問題

これも面白いなーと思った。こういうファイルを作って・・・

import java.time.chrono.JapaneseDate;
import java.time.chrono.JapaneseEra;

public class Hello {
  public static void main(String[] args) {
    System.out.println(JapaneseDate.of(JapaneseEra.HEISEI, 31, 4, 30));
    System.out.println(JapaneseDate.of(JapaneseEra.HEISEI, 31, 5, 1));
  }
}

8u181で実行するとなんともないんだけど

❯ docker run --rm -ti -v $(pwd)/Hello.java:/Hello.java openjdk:8u181-jdk sh -c 'javac Hello.java; java Hello'
Japanese Heisei 31-04-30
Japanese Heisei 31-05-01

11で実行すると・・・

❯ docker run --rm -ti -v $(pwd)/Hello.java:/Hello.java openjdk:11-jdk sh -c 'javac Hello.java; java Hello'
Japanese Heisei 31-04-30
Exception in thread "main" java.time.DateTimeException: year, month, and day not valid for Era
        at java.base/java.time.chrono.JapaneseDate.of(JapaneseDate.java:231)
        at Hello.main(Hello.java:7)

もうその日は平成じゃないよ!って怒られる。元号を使ってる場合は、頭の片隅に入れておいたほうが良さそうかな。

詳しくはこちら:

Javaの新元号対応を試す。そして実用には問題がある - きしだのはてな

来てよかった

色んな人に会えて嬉しかったなー。

エンジニアリング組織論への招待 Chapter1 思考のリファクタリングを読み終わった。

何回もいったりきたりしながら読んだ。

books.rakuten.co.jp

学びが多い。

Chapter1は、こんな感じかなー。

f:id:bufferings:20181007163213p:plain

テーマ的にそうなるのは仕方がないのかなーとは思うけど、ちょっと例とかがネガティブな印象あるので、心がそっちに引っ張られないように気をつけながら読んでた。面白かったー。続きも楽しみ!

尊敬する先輩がそう決めたからじゃなくて

僕がまだ小学生くらいの頃、上を向いて歯磨きをしてたら母親から「なんで上向いて歯磨きしとっと?前見ときー」って言われて、「は?幼稚園いっとーときにお母さんが上向いてしーちゅっとったけん上向いとるんちゃ!」「親に向かってちゃっちゃなんね!ばかたれ。あんときは背がちっちゃかったけん上向いとらんと口からこぼれとったんやろうも。もう今はこぼれんっちゃけん普通にしときー」

みたいなことを思い出したのが、後輩と話をしてて

「ここはこうした方が良いと思うんだけどどうしてそうしてるんですか?」って聞いたら「これは、尊敬する先輩がこのチームにいたときに決めたことなんです!」って言われて「ふーん。確かにその当時だったら分かる。で、今もそれが一番良いとあなたは思ってるんですか?」「え?僕ですか?」ってなったとき。

その尊敬する先輩が「何を決めたのか」じゃなくて「なぜそう決めたのか」を理解することが大切かなぁって思う。というのも、そのときの技術やトレンド、チームのスキルやプロジェクトの状況、マネージャの方針とか、そういった周りの状況を元に、そう決断されてるから。あと「今考えすぎても分からないし一旦これで挑戦する!」みたいなときもあるかな。

そういう周りの状況は常に変わってくから、その当時の決断が今の状況でも有効なのかどうかを考えるために、「何を決めたのか」じゃなくて「なぜそう決めたのか」を理解することが大切よなぁって思うのであった。

もうだいぶ北九州弁忘れたな。

あの、Regional Scrum Gathering Tokyo 2019のプロポーザル出したので、もし話聞いてみたいなーって思ってくれた方は投票お願いします(๑•̀ㅂ•́)و✧

confengine.com

仕事で「いいかげんにしてほしい」って話を聞くとき

仕事で「あの人は、どうして何度もそういうことをするんだろう?いいかげんにして欲しい」って愚痴を聞くときは「へー。それは大変だね」って返事をするんだけど、本当にこれまで何度も言葉にして伝えてるんかなぁ?とも思う。

もしかして(そういう態度好きじゃないんだけど、指摘して空気が悪くなるのも嫌だし、今回はガマンしようかな)って頭の中で考えて、何も言わずに見てたんじゃないかなぁ?とか。

(え?なんで?そういうの常識でしょ?しょーがないから自分で手直ししておくか)って何も伝えずに対応してたり。

そして何度か目のガマンのときに「もう限界!いいかげんにして!」っていきなり怒ったり、逆に(もういいや、期待するのをやめよう)って諦めたりしてたりしないかなぁ?とか。

それは、相手にとっては初めての指摘に見えているかもしれない。頭の中のことは言葉に出してくれないと、いくら考えてみても想像でしかないもんな。だから、自分もできるだけ伝えたいことは空気を気にせずに言葉にしていこう。

みたいなことを帰り道で考えたりしながら、でも、家に入るとそういうスイッチはオフになって、妻と過ごすときには言葉にされないことも感じ取っていけたらいいなって思うのであった。

Spring Boot Thin Launcherで依存ライブラリーをDockerイメージのレイヤーに閉じ込めてみる

昨日、Spring Boot Thin Launcherの話を書いたけど。

bufferings.hatenablog.com

最後に、でもFat JARと比べて何が嬉しいのかな?って考えて

Dockerのレイヤーにライブラリーを閉じ込めてしまえば、依存関係が変わらない限り再利用できるなぁ、って思った。それはまた次の機会に書こうと思う。

ということで、とりあえず閉じ込めてみた話。でも、これが役に立つかどうかは正直知らん。

## コードはこれ

https://github.com/bufferings/spring-boot-thin-sandbox

demo-thin-dockerディレクトリ。

こんな感じで依存ライブラリを含まないThin JARを作って

cd demo-thin-docker
❯ ./mvnw clean package

Dockerイメージをビルド

❯ docker build -t demo-thin-docker .

ビルド中に依存ライブラリーのダウンロードをするので、しばらく時間がかかる。

で、実行すると

❯ docker run -p 8080:8080 demo-thin-docker - thin.debug=true - thin.offline=true20180930 13:42:32.978 INFO 1 - - [ main] com.example.demo.DemoApplication : Started DemoApplication in 3.437 seconds (JVM running for 5.804)

という感じ。依存ライブラリーは既にダウンロード済みなので、普通の速さで起動する。

一度ビルドすると、依存ライブラリーを変更しないかぎり次からはそのレイヤーが再利用されるからビルドに時間はかからない。

## Spring Boot Thin Launcherのオプション

Dockerfileの説明をする前にSpring Boot Thin Launcherのオプションについて触れておく。ここに書いてる

https://github.com/dsyer/spring-boot-thin-launcher#command-line-options

色々あって面白いんだけど、その中で今回使ってるやつをピックアップ

thin.dryrun

アプリケーションを起動せずに、依存関係のダウンロードだけをする。

thin.offline

オフラインモードになる。ローカルのリポジトリーにあるやつだけを使う。ローカルになければ例外になる。

thin.archive

普通に起動すると、その起動したJARを実行しようとするんだけど、それとは別のJARを対象にしたい場合に使う。これを使った理由は後で説明する。

thin.debug

デバッグ情報がでる。

thin.trace

めっちゃ色々でる。あ、これは確認するときに使ったってだけで、今日紹介する手順の中では使ってなかった。

## Dockerfile

はこうなってる。マルチステージビルドを使ってる。

FROM openjdk:11-jdk-slim
COPY docker/spring-boot-thin-wrapper-1.0.15.RELEASE.jar /thin/wrapper.jar
COPY pom.xml /thin/pom.xml
WORKDIR /thin
RUN jar cvf pom.jar pom.xml
RUN java -jar wrapper.jar \
     --thin.archive=/thin/pom.jar \
     --thin.dryrun=true \
     --thin.debug=true

FROM openjdk:11-jre-slim
COPY --from=0 /root/.m2 /root/.m2
COPY target/demo-thin-docker-0.0.1-SNAPSHOT.jar /app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

いっこずつ説明していく。

## FROM

FROM openjdk:11-jdk-slim

特に理由はないけど11を使ってみた。

## COPY wrapper.jar

COPY docker/spring-boot-thin-wrapper-1.0.15.RELEASE.jar /thin/wrapper.jar

対象のJARがまだない状態で、pom.xmlを元に依存ライブラリーだけダウンロードしてきたいなーと思ったので、その機能(ThinJarWrapper)を持ってるwrapper.jarをダウンロードしておいた。

## COPY pom.xml

COPY pom.xml /thin/pom.xml
WORKDIR /thin
RUN jar cvf pom.jar pom.xml

プロジェクトルートからpom.xmlをコピーしてJARに固める。wrapper.jarpom.xmlを直接読めたらいいなーって思ったんだけど、ソースを見てみたら、どうもJARじゃないとだめみたいなので、pom.xmlだけを持ったpom.jarを作ることにした。

## dry-run

RUN java -jar wrapper.jar \
     --thin.archive=/thin/pom.jar \
     --thin.dryrun=true \
     --thin.debug=true

dry-runモードでwrapper.jarを実行する。でも対象はwrapper.jarじゃなくてpom.jarの中のpom.xmlにしたいので、thin.archiveで指定。

thin.rootオプションを指定していないので、デフォルトの.m2ディレクトリに依存ライブラリーがダウンロードされる。

## COPY .m2

FROM openjdk:11-jre-slim
COPY --from=0 /root/.m2 /root/.m2

依存ライブラリーのダウンロードが終わったら、その.m2ディレクトリーだけがあればいい。wrapper.jarpom.jarは要らないので、別のイメージをFROMにして.m2だけを持ってくる。

## COPY app.jar and run

COPY target/demo-thin-docker-0.0.1-SNAPSHOT.jar /app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

で、あとはいつも通りアプリのJARをコピーしてきて、エントリーポイントを書いておく。

## Offline Mode

実行するときにダウンロードしてきてないことを確認するためにオフラインモードで起動してみた

❯ docker run -p 8080:8080 demo-thin-docker - thin.debug=true - thin.offline=true

実際にはここで何度かエラーが出て、試行錯誤したのであった(thin.propertiesを使う方法があって、最初はそっちでやってたんだけど、それだとランチャーがダウンロードされてなくて、ここで「ランチャーがないから無理!」って怒られたのであった。ごにょっとしたらいけたんだけど、それより素直にpomを使うことにした。)

## という感じ!

今のところThin Launcherは使うつもりはないけど、何かで「依存ライブラリーを切り離したいな」って思ったときには、この記事を読みなおしてやってみるかもなぁというくらい。

いい感じの用途を思いついたら教えてください!これでThin Launcher遊びはおしまい!