- 文字列結合
- StringBuilder
- StringBuilder 簡略版
- commons.lang StringUtils
- Guava Joiner
- Java8 String
- Collectors joining
- StringJoiner
- collect with StringJoiner
- JMH ベンチマーク測定ツール
- Gradle ビルドスクリプトの作成
- ベンチマーク用ソースの作成
- ベンチマークの実行
- JMH オプション
- JMH オプションの補足説明
- アノテーション
- メソッドチェーン
- コマンドラインオプション
List<String> list = Arrays.asList("A", "B", "C", "D", "E", "F", "G", "H", "I");
カンマ区切りの文字列に変換
A, B, C, D, E, F, G, H, I
いろいろなやり方がある
文字列結合
String result = ""; for (String s : list) result += result.isEmpty() ? s : ", " + s;
StringBuilder
StringBuilder sb = new StringBuilder(); for (int i = 0; i< list.size(); i++) { if (i > 0) { sb.append(", "); } sb.append(list.get(i)); } result = sb.toString();
StringBuilder 簡略版
StringBuilder sb = new StringBuilder(); for (String s : list) sb.append(sb.length() == 0 ? s : ", " + s);
commons.lang StringUtils
String result = StringUtils.join(list, ", ");
Guava Joiner
String result = Joiner.on(", ").join(list);
Java8 String
String result = String.join(", ", list);
Collectors joining
String result = list.stream().collect(Collectors.joining(", "));
StringJoiner
StringJoiner joiner = new StringJoiner(", "); list.forEach(e -> joiner.add(e));
collect with StringJoiner
String result = list.stream().collect( () -> new StringJoiner(", "), StringJoiner::add, StringJoiner::merge).toString();
いろいろあるが、パフォーマンスはどうなのか?
JMH ベンチマーク測定ツール
Java でのベンチマークは JITコンパイラによる最適化やGCの影響など、いろいろと考慮すべき点が多くて難しい。
そんな時はOpenJDKで提供されているベンチマーク測定ツール JMH(Java Microbenchmark Harness) を使うと良い。マイクロベンチ界のJUnitといった趣き。
このツールを使うと、@Benchmark
でアノテートしたメソッドを元に、マイクロベンチ用のコードを生成し、ベンチマーク結果を収集できる。
通常は jar を作成して実行することで、より信頼度の高いベンチマーク結果を得ることができるが、IDE から実行したり、Maven や Gradle から実行できるプラグインも存在する。
ここでは Gradle プラグインを使って、前述の文字列連結のベンチマークを取得してみよう。
Gradle ビルドスクリプトの作成
Gradle 2.1 以降であれば、以下のようにプラグインを指定すれば良い。
plugins { id "me.champeau.gradle.jmh" version "0.4.4" }
昔ながらのプラグイン指定の場合は以下のようにする。
buildscript { repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.3" } } apply plugin: "me.champeau.gradle.jmh"
build.gradle
全体は以下のようになる。
plugins { id 'java' id "me.champeau.gradle.jmh" version "0.4.3" } repositories { jcenter() } dependencies { jmh 'org.apache.commons:commons-lang3:3.6' jmh 'com.google.guava:guava:22.0' }
プラグインの Configuration により jmh が作成されるため、ベンチマークで必要なライブラリの依存は jmh で読み込む。
ベンチマーク用ソースの作成
JMH では、src/jmh
以下にあるソースをベンチマーク対象とみなす。よって、ベンチマーク用のソースファイルは src/jmh/java/
配下にパッケージを作成する(パッケージ名を指定しないデフォルトパッケージにした場合はエラーになるので注意)。
ベンチマーク対象のメソッドには @Benchmark
を付ければよい。以下のようなる。
package etc9; import com.google.common.base.Joiner; import org.apache.commons.lang3.StringUtils; import org.openjdk.jmh.annotations.Benchmark; import java.util.Arrays; import java.util.List; import java.util.StringJoiner; import java.util.stream.Collectors; public class MicroBench { private static final List<String> list = Arrays.asList( "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"); @Benchmark public void simpleString() { String result = ""; for (String s : list) result += result.isEmpty() ? s : ", " + s; } @Benchmark public void stringBuilder() { StringBuilder sb = new StringBuilder(); for (int i = 0; i< list.size(); i++) { if (i > 0) { sb.append(", "); } sb.append(list.get(i)); } sb.toString(); } @Benchmark public void simpleStringBuilder() { StringBuilder sb = new StringBuilder(); for (String s : list) sb.append(sb.length() == 0 ? s : ", " + s); sb.toString(); } @Benchmark public void commonsLang() { String result = StringUtils.join(list, ", "); } @Benchmark public void guvavJoiner() { String result = Joiner.on(", ").join(list); } @Benchmark public void java8String() { String result = String.join(", ", list); } @Benchmark public void joining() { String result = list.stream().collect(Collectors.joining(", ")); } @Benchmark public void stringJoiner() { StringJoiner joiner = new StringJoiner(", "); list.forEach(e -> joiner.add(e)); } @Benchmark public void collectWithStringJoiner() { String result = list.stream().collect( () -> new StringJoiner(", "), StringJoiner::add, StringJoiner::merge).toString(); } }
メソッドに @Benchmark
を付与することでベンチマーク対象となる。
ベンチマークの実行
以下でベンチマークが実行できる。
$ ./gradlew clean jmh
デフォルトだと1秒当たりの実行回数がスループットとしてスコア化される。
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
MicroBench.stringBuilder | thrpt | 200 | 2681020.541 | ± 24225.350 | ops/s |
MicroBench.commonsLang | thrpt | 200 | 2264534.146 | ± 25666.285 | ops/s |
MicroBench.guvavJoiner | thrpt | 200 | 2211854.864 | ± 22777.299 | ops/s |
MicroBench.java8String | thrpt | 200 | 2195035.344 | ± 19511.354 | ops/s |
MicroBench.stringJoiner | thrpt | 200 | 2121539.080 | ± 13531.781 | ops/s |
MicroBench.simpleStringBuilder | thrpt | 200 | 2110835.816 | ± 32823.739 | ops/s |
MicroBench.collectWithStringJoiner | thrpt | 200 | 1860335.526 | ± 24097.640 | ops/s |
MicroBench.joining | thrpt | 200 | 1851112.885 | ± 19930.058 | ops/s |
MicroBench.simpleString | thrpt | 200 | 901608.887 | ± 15150.450 | ops/s |
文字列配列の要素数も少ないのでなんとも言えないが、やっぱり文字列連結は遅い。後は大した差はないかな。
ちなみに9個のメソッドのベンチマークをデフォルト設定で取ると、ウォームアップ×20 + 計測×20 × 10回繰り返し × 9メソッドで 1時間くらいかかる。測定条件を変えるにはオプション設定を行う。
JMH オプション
JMH のオプションは、build.gradle
で、例えば以下のように指定できる。
jmh { iterations = 10 resultFormat = 'CSV' }
JMH のオプションは以下の指定が可能。
プロパティ | 例 | 説明 |
---|---|---|
include | ['.*'] | ベンチマークに含めるファイルを正規表現で指定。'クラス名.メソッド名' でマッチング判定される。 |
exclude | ベンチマークから除外するファイルを正規表現で指定。'クラス名.メソッド名' でマッチング判定される。 | |
iterations | 10 | 計測の繰り返し回数(デフォルト20回) |
benchmarkMode | ['thrpt'] | ベンチマークモードを指定。秒間の実行スループット(thrpt)、1実行の平均時間(avgt)、最大/最小を含む実行時間(sample)、実行時間(1回のみのシングルショット)(ss)、 全て(all)、を指定 |
batchSize | 1 | オペレーション毎にベンチマークメソッドを呼ぶ回数(デフォルト1回)。いくつかの計測モードでは設定値は無効になる。 |
fork | 10 | 1回のベンチマーク測定時に fork する回数。 |
failOnError | false | エラー発生時にベンチマークを失敗にするかどうか |
forceGC | false | イテレーション毎に強制的にGCを行うか |
jvm | fork 時に使用するカスタムjvmを指定 | |
jvmArgs | fork 時に使用するカスタムjvmの引数 | |
jvmArgsAppend | fork 時のjvm引数 (末尾に追加)を指定 | |
jvmArgsPrepend | fork 時のjvm引数 (先頭に追加)を指定 | |
humanOutputFile | project.file("human.txt") | human-readable 出力結果ファイル |
resultsFile | project.file("results.txt") | 結果ファイル |
operationsPerInvocation | 1 | ワーカースレッドが実行するベンチマーク対象の実行回数(後述)。 |
benchmarkParameters | [:] | ベンチマークパラメータ |
profilers | 追加で収集するプロファイル [cl, comp, gc, stack, perf, perfnorm, perfasm, xperf, xperfasm, hs_cl, hs_comp, hs_gc, hs_rt, hs_thr] | |
timeOnIteration | '1s' | 1回の繰り返しにかける時間(デフォルト1s) |
resultFormat | 'CSV' | 結果ファイルのフォーマット。CSV, JSON, NONE, SCSV, TEXTが指定できる。 |
synchronizeIterations | false | 繰り返しを同期するかどうか |
threads | 1 | ベンチマーク時のワーカースレッド数(デフォルト1)。2とした場合は、ベンチマークメソッドをtimeOnIteration の時間中2スレッドで実行するため、通常はスループットが高くなる。 |
threadGroups | [2,3,4] | 非同期のベンチマークでスレッドグループディストリビューションの上書きを指定 |
timeout | '10m' | イテレーションのタイムアウト |
timeUnit | 'ms' | 結果の単位(デフォルト ms) |
verbosity | 'NORMAL' | 詳細出力。SILENT、NORMAL、EXTRA のいずれかを指定する。 |
warmup | '1s' | ウォームアップ毎の時間(デフォルト1s) |
warmupBatchSize | 1 | ウォームアップオペレーション毎にベンチマークメソッドを呼ぶ回数(デフォルト1回) |
warmupForks | 0 | 1回のウォームアップにおいてforkする回数。0 とした場合にはウォームアップ時の fork は行わない。 |
warmupIterations | 20 | ウォームアップの繰り返し回数(デフォルト20回) |
warmupMode | 'INDI' | ウォームアップモード。INDI(individual) 各ベンチマーク毎に独立してウォームアップを実施、BULKとした場合ベンチマーク開始前にバルクでウォームアップを実施、BULK_INDI とした場合は双方実施 |
warmupBenchmarks | ['.*Warmup'] | 追加で実施するウォームアップ操作を指定 |
zip64 | true | 大きなアーカイブで ZIP64 フォーマットを使うかどうか |
jmhVersion | '1.19' | Jmh のバージョンを指定する(現時点でのデフォルト1.17) |
includeTests | false | テストソースを生成する jmh の jar に含めるかどうか |
duplicateClassesStrategy | 'fail' | jmhJar タスクでクラスの重複が発生した場合のストラテジ |
JMH オプションの補足説明
オプションが何を意味するのかはコード見た方が早いので、分かりにくそうな箇所を補足。
fork
jmh はベンチマーク取得の際に、java コマンドを java.lang.ProcessBuilder
経由で発行してベンチマークを実行している。
for (int i = 0; i < forkCount; i++) { List<String> forkedString = getForkedMainCommand(...); long startTime = System.currentTimeMillis(); List<IterationResult> result = doFork(...); if (!result.isEmpty()) { BenchmarkResultMetaData md = server.getMetadata(); BenchmarkResult br = new BenchmarkResult(params, result, md); results.put(params, br); }
fork はこの際のループ回数として使われます。
iterations
繰り返し回数は、前述の fork によりForkedMain
が実行され、 ForkedRunner
の親クラス BaseRunner
にある runBenchmark 内でのループ回数になる。かなり省略しますが、以下のようになっています。
protected void runBenchmark(BenchmarkParams benchParams, BenchmarkHandler handler, IterationResultAcceptor acceptor) { long measurementTime = System.currentTimeMillis(); IterationParams mp = benchParams.getMeasurement(); for (int i = 1; i <= mp.getCount(); i++) { if (runSystemGC()) { out.verbosePrintln("System.gc() executed"); } boolean isLastIteration = (i == mp.getCount()); IterationResult ir = handler.runIteration(benchParams, mp, isLastIteration); long stopTime = System.currentTimeMillis(); }
ループ条件の mp.getCount()
がiterations で指定した繰り返し回数になります。
そして、BenchmarkHandler#runIteration()
内で実際のイテレーション操作が行われます。
threads
スレッドは繰り返し内における BenchmarkTask
の数になっています。
public IterationResult runIteration(BenchmarkParams benchmarkParams, IterationParams params, boolean last) { int numThreads = benchmarkParams.getThreads(); TimeValue runtime = params.getTime(); InfraControl control = new InfraControl(benchmarkParams, params, preSetupBarrier, preTearDownBarrier, last, new Control()); // preparing the worker runnables BenchmarkTask[] runners = new BenchmarkTask[numThreads]; for (int i = 0; i < runners.length; i++) { runners[i] = new BenchmarkTask(control); } // submit tasks to threadpool List<Future<BenchmarkTaskResult>> completed = new ArrayList<>(); CompletionService<BenchmarkTaskResult> srv = new ExecutorCompletionService<>(executor); for (BenchmarkTask runner : runners) { srv.submit(runner); } ... }
環境によってスレッド実行の方法は変わりますが、threads で指定したスレッド数が maxThreads
としてスレッドプールを作成されており、
Executors.newFixedThreadPool(maxThreads, new WorkerThreadFactory(prefix));
ExecutorCompletionService
を経由して BenchmarkTask
が実行されます。
batchSize
BenchmarkTask
から jmh が生成したベンチマーク用コードのメソッドが呼ばれます。そのメソッド内にて、実際のメソッド呼び出しのループ条件として batchSize
が使われます。
for (int b = 0; b < batchSize; b++) { if (control.volatileSpoiler) return; l_microbench0_0.java8String(); }
バッチサイズは、計測モードがシングルショット(ss)やサンプル(sample)の際に、実際のベンチマークメソッドを呼ぶ際のループ回数として使われます。
operationsPerInvocation
ベンチマーク実行を制御するパラメータに見えますが、ベンチマーク結果値を調整するための値です。
以下のようにベンチマークメソッド内でループさせた場合、10 と設定しておくと実行時間などが1/10された値に調整されます。
@Benchmark @OperationsPerInvocation(10) public void test() { for (int i = 0; i < 10; i++) { // do something } }
アノテーション
ベンチマークメソッドにはアノテーションにて計測方法を上書き設定できる。
@Benchmark @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS) public double measure() { return Math.log(x1); }
利用可能なアノテーションは以下のようなものがある。
アノテーション | 説明 |
---|---|
@Benchmark |
ベンチマーク対象メソッドに付与 |
@Setup |
ベンチマーク実行前の設定メソッドに付与 |
@TearDown |
ベンチマーク実行後の後処理メソッドに付与 |
@BenchmarkMode |
ベンチマークメソッドを指定。Mode.Throughput , Mode.AverageTime , Mode.SampleTime , Mode.SingleShotTime , Mode.All を指定 |
@Measurement |
計測時のパラメータを設定。設定できる項目は、iterations, time, timeUnit, batchSize |
@Warmup |
ウォームアップ時のパラメータを設定。設定できる項目は、iterations, time, timeUnit, batchSize |
@OutputTimeUnit |
結果レポートの出力単位TimeUnit を指定 |
@Timeout |
タイムアウト時間を指定 |
@Fork |
fork 回数や、fork 時の jvmオプションを指定 |
@CompilerControl |
fork 時のコンパイラオプションを指定(インライン展開を強制など) |
@Threads |
スレッド数を指定 |
@Group |
非同期ベンチマークで複数メソッドをグルーピング指定する |
@GroupThreads |
グループ内のメソッドを実行するスレッド数を指定する |
@OperationsPerInvocation |
前述の operationsPerInvocation パラメータを参照 |
@Param |
後述 |
@State |
後述 |
@Param アノテーション
以下のようにベンチマーク時にパラメータを供給する
@Param({"1", "31", "65", "101", "103"}) public int arg; @Param({"0", "1", "2", "4", "8", "16", "32"}) public int certainty; @Benchmark public boolean bench() { return BigInteger.valueOf(arg).isProbablePrime(certainty); }
@State アノテーション
ベンチマーク時に状態を管理するオブジェクトを指定する
@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++; }
スコープ設定に応じて状態を保持できる。
メソッドチェーン
オプションは OptionsBuilder
でメソッドチェーンで指定できる。
public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JMHSample_03_States.class.getSimpleName()) .warmupIterations(5) .measurementIterations(5) .threads(4) .forks(1) .build(); new Runner(opt).run(); }
ベンチマーククラス内に main メソッドを用意してオプション指定して実行することができる。
コマンドラインオプション
以下でJMHランタイムを含むjarが生成できる。
$ ./gradlew jmhJar
作成されたjarは以下のように実行できる。
$ java -jar libs/benchmarks.jar -wi 5 -i 5
ベンチマーク対象クラスBenchmark
に以下のようなmainメソッドを作成した場合、
public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder() .include(Benchmark.class.getSimpleName()) .forks(1) .build(); new Runner(options).run(); }
以下のように実行することでベンチマーク対象を個別に実行できる。
$ java -cp target/benchmark.jar etc9.Benchmark
指定できるコマンドラインオプションは以下。
Usage: java -jar ... [regexp*] [options] [opt] means optional argument. <opt> means required argument. "+" means comma-separated list of values. "time" arguments accept time suffixes, like "100ms". [arguments] Benchmarks to run (regexp+). -bm <mode> Benchmark mode. Available modes are: [Throughput/thrpt, AverageTime/avgt, SampleTime/sample, SingleShotTime/ss, All/all] -bs <int> Batch size: number of benchmark method calls per operation. Some benchmark modes may ignore this setting, please check this separately. -e <regexp+> Benchmarks to exclude from the run. -f <int> How many times to fork a single benchmark. Use 0 to disable forking altogether. Warning: disabling forking may have detrimental impact on benchmark and infrastructure reliability, you might want to use different warmup mode instead. -foe <bool> Should JMH fail immediately if any benchmark had experienced an unrecoverable error? This helps to make quick sanity tests for benchmark suites, as well as make the automated runs with checking error codes. -gc <bool> Should JMH force GC between iterations? Forcing the GC may help to lower the noise in GC-heavy benchmarks, at the expense of jeopardizing GC ergonomics decisions. Use with care. -h Display help. -i <int> Number of measurement iterations to do. Measurement iterations are counted towards the benchmark score. -jvm <string> Use given JVM for runs. This option only affects forked runs. -jvmArgs <string> Use given JVM arguments. Most options are inherited from the host VM options, but in some cases you want to pass the options only to a forked VM. Either single space-separated option line, or multiple options are accepted. This option only affects forked runs. -jvmArgsAppend <string> Same as jvmArgs, but append these options before the already given JVM args. -jvmArgsPrepend <string> Same as jvmArgs, but prepend these options before the already given JVM arg. -l List the benchmarks that match a filter, and exit. -lp List the benchmarks that match a filter, along with parameters, and exit. -lprof List profilers. -lrf List machine-readable result formats. -o <filename> Redirect human-readable output to a given file. -opi <int> Override operations per invocation, see @OperationsPerInvocation Javadoc for details. -p <param={v,}*> Benchmark parameters. This option is expected to be used once per parameter. Parameter name and parameter values should be separated with equals sign. Parameter values should be separated with commas. -prof <profiler> Use profilers to collect additional benchmark data. Some profilers are not available on all JVMs and/or all OSes. Please see the list of available profilers with -lprof. -r <time> Minimum time to spend at each measurement iteration. Benchmarks may generally run longer than iteration duration. -rf <type> Format type for machine-readable results. These results are written to a separate file (see -rff). See the list of available result formats with -lrf. -rff <filename> Write machine-readable results to a given file. The file format is controlled by -rf option. Please see the list of result formats for available formats. -si <bool> Should JMH synchronize iterations? This would significantly lower the noise in multithreaded tests, by making sure the measured part happens only when all workers are running. -t <int> Number of worker threads to run with. 'max' means the maximum number of hardware threads available on the machine, figured out by JMH itself. -tg <int+> Override thread group distribution for asymmetric benchmarks. This option expects a comma-separated list of thread counts within the group. See @Group/@GroupThreads Javadoc for more information. -to <time> Timeout for benchmark iteration. After reaching this timeout, JMH will try to interrupt the running tasks. Non-cooperating benchmarks may ignore this timeout. -tu <TU> Override time unit in benchmark results. Available time units are: [m, s, ms, us, ns]. -v <mode> Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA] -w <time> Minimum time to spend at each warmup iteration. Benchmarks may generally run longer than iteration duration. -wbs <int> Warmup batch size: number of benchmark method calls per operation. Some benchmark modes may ignore this setting. -wf <int> How many warmup forks to make for a single benchmark. All iterations within the warmup fork are not counted towards the benchmark score. Use 0 to disable warmup forks. -wi <int> Number of warmup iterations to do. Warmup iterations are not counted towards the benchmark score. -wm <mode> Warmup mode for warming up selected benchmarks. Warmup modes are: INDI = Warmup each benchmark individually, then measure it. BULK = Warmup all benchmarks first, then do all the measurements. BULK_INDI = Warmup all benchmarks first, then re-warmup each benchmark individually, then measure it. -wmb <regexp+> Warmup benchmarks to include in the run in addition to already selected by the primary filters. Harness will not measure these benchmarks, but only use them for the warmup.