言語横断ベンチマーク設計
C#・C++・Java・Goのように異なる言語・ランタイムの実装間で、測定条件を揃えて実行速度を公平に比較するための設計手法。
言語間の性能比較
マイクロソフトが提供する、.NET上で動作するオブジェクト指向のプログラミング言語。
C#言語
JIT(Just-In-Time)コンパイル
実行時に中間コード(.NETのILやJavaのバイトコードなど)からネイティブコードを生成するコンパイル方式。
実行時JIT
Java
JVM(Java仮想マシン)上でバイトコードを実行時にJITコンパイルして動作するオブジェクト指向プログラミング言語。
Javaプログラミング言語
C++
コンパイル時にネイティブコードへ事前コンパイル(AOT)される、ISO標準のシステムプログラミング言語。
AOT(事前)コンパイル
プログラムを実行する前に、ソースコードをあらかじめネイティブの機械語へ変換しておく方式。
事前コンパイル
Googleが開発した、実行前にネイティブの機械語へ事前コンパイルされるプログラミング言語。
Go言語
Tiered Compilation
.NETのJITが、まず素早く生成した層のコードで実行し、よく呼ばれるメソッドだけ後から最適化し直す2段階のコンパイル方式。
階層化コンパイル
GOMAXPROCS
Goのランタイムが、Goのコードを同時に実行できるOSスレッド数の上限を設定する仕組み。
cgo
GoのコードからCのコードを呼び出せるようにするGoの仕組み。有効にすると呼び出しコストやビルド条件が変わる。
BenchmarkDotNet
C#(.NET)向けの、ウォームアップや統計処理を自動で行うマイクロベンチマークライブラリ。
JMH(Java Microbenchmark Harness)
OpenJDKが提供する、Java向けのマイクロベンチマークを作成・実行するためのハーネス。
go test -bench と benchstat
Go標準のtestingパッケージが提供するベンチマーク実行機能(go test -bench)と、その結果を統計的に比較するbenchstatツールの組み合わせ。
Google Benchmark
Googleが提供する、C++向けのマイクロベンチマークライブラリ。
ウォームアップ後の安定状態計測(warm)
プロセス起動直後を含むcoldな状態と区別して、数回の実行を経て安定した後の状態だけを対象に行う性能測定。
デッドコード除去(最適化による処理の消滅)
コンパイラが、結果が使用されないと判断した計算やコードを最適化によって丸ごと取り除いてしまうこと。
最適化による処理消去
checksumによる正しさ確認
比較対象の全実装が同じ入力から同じ結果(checksum)を返すことを確認する、速度比較に先立つ検証手順。
正しさ確認
結果検証
テールレイテンシのパーセンタイル(p95/p99)
全実行を速い順に並べたときの95%地点・99%地点の値で、平均には表れにくい遅い応答の分布を示す指標。
ガベージコレクション(GC)
プログラムが使わなくなったメモリ上のオブジェクトを自動的に検出し、解放するランタイムの仕組み。
JIT前提の.NETランタイムで実行する場合、C#(.NET)はCILを実行時にJITコンパイラでネイティブコードへ変換しながら実行する。Native AOTで発行した場合は事前コンパイルされ実行時のJITは使われない
HotSpot VMのようなJIT前提の実行環境では、Javaはバイトコードを実行時にJITコンパイルする。native-imageなどで事前コンパイルした場合は当てはまらない
C++はコンパイラが実行前にソースコードをネイティブの実行ファイルまで変換する事前コンパイル方式を取る。
Goのプログラムは実行前にネイティブの機械語へコンパイルされ、JITを持たない。
.NETのJITは、まず素早くコンパイルする層で実行し、よく呼ばれるメソッドだけ後から最適化し直すTiered Compilationを既定で備える。
GoランタイムはGOMAXPROCSでGoコードを同時実行するOSスレッド数の上限を制御する。
CのコードをGoから呼び出す場合に限り、GoはcgoによってCのコードを呼び出せるが、有効にすると呼び出しコストやビルド条件が変わる。
C#で言語内マイクロベンチマークを書く場合には、BenchmarkDotNetを使う。
Javaで言語内マイクロベンチマークを書く場合には、OpenJDKのJMHを使う。
Goで言語内ベンチマークを書く場合には、testingパッケージのgo test -benchとbenchstatを使う。
C++で言語内マイクロベンチマークを書く場合には、Google Benchmarkを使う。
BenchmarkDotNetは複数回のイテレーションとウォームアップを経て安定した測定値を得る仕組みを標準で備える。
JMHは@Warmupアノテーションでウォームアップの反復回数と時間を明示的に指定する仕組みを持つ。
Google BenchmarkのDoNotOptimize/ClobberMemoryのような最適化抑止の仕組みで結果を消費させた場合に限り、コンパイラが未使用と判断した計算を消し去ることを防げる。単に使わないローカル変数へ結果を入れるだけでは消される
C++の最適化コンパイラは、計算結果が使用されないと判断すると、その処理自体を丸ごと消してしまうことがある。
計算結果をchecksumとして出力・使用させることで、コンパイラによる最適化除去を防ぐ。
分布の評価には平均だけでなくp95/p99のようなテールレイテンシのパーセンタイルを使う。
.NETはガベージコレクターがオブジェクトの割り当てと解放を管理する。
JavaはHotSpot VMのガベージコレクション方式でメモリのライフサイクルを管理する。
Goランタイムはトレーシング方式のガベージコレクターでヒープを管理する。
JITの影響を受けるC#やJavaを含む比較では、ウォームアップ後の安定状態を初回実行と区別して計測することが前提になる。
比較対象の全実装が同じ入力から同じ結果(checksum)を返すことを確認してから速度を比較する。
C#とJavaはJITの影響を受けるため、比較設計はJITの扱いを踏まえる必要がある。
C++とGoは通常事前コンパイル済みであり、これがJIT言語との比較設計上の違いを生む。