実装やコンパイラの性能次第で幾らでも変わると思うのだが、プログラミング言語の速度については、大体以下のようにグループ分けして考えている。
同グループ内であれば速度は似たり寄ったりなので「どっちが速いのか」という思索に至ることはなく、例えば「JavaやC#のどちらが速いのか?」という点についてはこれまであまり意識することはなかった。
ところが、今日こんな記事を見た。
JavaとC#の「int」の比較 - Qiita
有り難いことに、昔書いたエントリーが紹介されており、面倒臭かったから探しもせず、計測もしなかったJavaのプリミティブ型とC#のデータ型のパフォーマンスを調べて頂いており、5万回ずつ単純な数値型を増減する処理を実行しており、最後にはこういう結果が出ていた。
Java : 2783ms.
C# : 15236ms.
という結果が出ました!
これは少し驚きだ。
というのもC#の方が6-7倍も遅いのだ。俺の言語速度のヒエラルキー図が完全に崩れてしまう。とりわけ上の記事で実行されていたC#コードは、構造体からオブジェクトへのボックス化が発生することはないため、C#が特段不利になるようなものでもないと解される。
だが、いざ実行されたコードを試してみると、その理由が理解できた。というのも、上記の結果はデバッグビルトのものであり、恐らくVisual Studio上で実行したものだと解される。だからC#にものすごく不利な条件で測定されたものと解される。どうして不利かというと、要はJITコンパイルで最適化されていないのである。
上記のコードをデバッグとリリースモードでC#コンパイラがILにしたものが以下である。
Debug
ILを見る
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// コード サイズ 138 (0x8a)
.maxstack 2
.locals init ([0] int32 ans,
[1] class [System]System.Diagnostics.Stopwatch sw,
[2] int32 j,
[3] int32 i,
[4] bool CS$4$0000)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: newobj instance void [System]System.Diagnostics.Stopwatch::.ctor()
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_000f: nop
IL_0010: ldc.i4.0
IL_0011: stloc.2
IL_0012: br.s IL_0058
IL_0014: nop
IL_0015: ldc.i4.1
IL_0016: stloc.3
IL_0017: br.s IL_0023
IL_0019: nop
IL_001a: ldloc.0
IL_001b: ldloc.3
IL_001c: add
IL_001d: stloc.0
IL_001e: nop
IL_001f: ldloc.3
IL_0020: ldc.i4.1
IL_0021: add
IL_0022: stloc.3
IL_0023: ldloc.3
IL_0024: ldc.i4 0xc350
IL_0029: cgt
IL_002b: ldc.i4.0
IL_002c: ceq
IL_002e: stloc.s CS$4$0000
IL_0030: ldloc.s CS$4$0000
IL_0032: brtrue.s IL_0019
IL_0034: ldc.i4.1
IL_0035: stloc.3
IL_0036: br.s IL_0042
IL_0038: nop
IL_0039: ldloc.0
IL_003a: ldloc.3
IL_003b: sub
IL_003c: stloc.0
IL_003d: nop
IL_003e: ldloc.3
IL_003f: ldc.i4.1
IL_0040: add
IL_0041: stloc.3
IL_0042: ldloc.3
IL_0043: ldc.i4 0xc350
IL_0048: cgt
IL_004a: ldc.i4.0
IL_004b: ceq
IL_004d: stloc.s CS$4$0000
IL_004f: ldloc.s CS$4$0000
IL_0051: brtrue.s IL_0038
IL_0053: nop
IL_0054: ldloc.2
IL_0055: ldc.i4.1
IL_0056: add
IL_0057: stloc.2
IL_0058: ldloc.2
IL_0059: ldc.i4 0xc350
IL_005e: clt
IL_0060: stloc.s CS$4$0000
IL_0062: ldloc.s CS$4$0000
IL_0064: brtrue.s IL_0014
IL_0066: ldloc.1
IL_0067: callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
IL_006c: nop
IL_006d: ldstr "{0}ms."
IL_0072: ldloc.1
IL_0073: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds()
IL_0078: box [mscorlib]System.Int64
IL_007d: call void [mscorlib]System.Console::WriteLine(string, object)
IL_0082: nop
IL_0083: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0088: pop
IL_0089: ret
} // end of method Program::Main
Release
ILを見る
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// コード サイズ 109 (0x6d)
.maxstack 2
.locals init ([0] int32 ans,
[1] class [System]System.Diagnostics.Stopwatch sw,
[2] int32 j,
[3] int32 i,
[4] int32 V_4)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: newobj instance void [System]System.Diagnostics.Stopwatch::.ctor()
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_000e: ldc.i4.0
IL_000f: stloc.2
IL_0010: br.s IL_0043
IL_0012: ldc.i4.1
IL_0013: stloc.3
IL_0014: br.s IL_001e
IL_0016: ldloc.0
IL_0017: ldloc.3
IL_0018: add
IL_0019: stloc.0
IL_001a: ldloc.3
IL_001b: ldc.i4.1
IL_001c: add
IL_001d: stloc.3
IL_001e: ldloc.3
IL_001f: ldc.i4 0xc350
IL_0024: ble.s IL_0016
IL_0026: ldc.i4.1
IL_0027: stloc.s V_4
IL_0029: br.s IL_0036
IL_002b: ldloc.0
IL_002c: ldloc.s V_4
IL_002e: sub
IL_002f: stloc.0
IL_0030: ldloc.s V_4
IL_0032: ldc.i4.1
IL_0033: add
IL_0034: stloc.s V_4
IL_0036: ldloc.s V_4
IL_0038: ldc.i4 0xc350
IL_003d: ble.s IL_002b
IL_003f: ldloc.2
IL_0040: ldc.i4.1
IL_0041: add
IL_0042: stloc.2
IL_0043: ldloc.2
IL_0044: ldc.i4 0xc350
IL_0049: blt.s IL_0012
IL_004b: ldloc.1
IL_004c: callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
IL_0051: ldstr "{0}ms."
IL_0056: ldloc.1
IL_0057: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds()
IL_005c: box [mscorlib]System.Int64
IL_0061: call void [mscorlib]System.Console::WriteLine(string, object)
IL_0066: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_006b: pop
IL_006c: ret
} // end of method Program::Main
わざわざ長いコードを貼って恐縮ではあるが、生成されるILにあまり大きな違いはない。}
の部分でもブレークポイントを設置できるようにILでnop(No Operation)命令が埋め込まれていたりするが、これだけでは流石に大きな違いは出てこない。一番の違いは、この後更にJITコンパイラがネイティブコードに変換するときに、メソッドのインライン展開やレジスタ割り当てなどの最適化が実施される。特にこのコード例の場合、レジスタ割り当てがかなり効きそう。
ところで、比較するならJavaについても調べないといけないが、C#みたいにIDEで実行したかどうかで違いがあるのかどうか分からんが、とりあえずコマンドラインでjavacして実行してみると・・・「3ms」とか「4ms」とか異様に速いんだけどッ!? …ってコードをよく見たら、恐らくVMが最終的にans
というローカル変数が使われないからって最初っから計算していない可能性が高い。という訳でコードを以下のように修正してもう一度実行したらちゃんとした値は出た。この辺りはJavaのVMの方が頭が良いとは感じた。
public class TestOfInt {
@param args
public static void main(String[] args) {
int ans = 0;
long start = System.currentTimeMillis();
for(int j = 0; j < 50000; j++) {
for(int i = 1; i <= 50000; i++) {
ans += i;
}
for(int i = 1; i <= 50000; i++) {
ans -= i;
}
}
long end = System.currentTimeMillis();
System.out.println(end - start + "ms. " + ans);
}
}
最終的な結果はこうなった。なおCore™ i7-3770KとDDR3 16GB。