って、関数を呼び出す事によるオーバーヘッドよりも値型<->参照型のboxing/unboxingによるものが大きいのではないかとふと思いついたので、以下のような簡単なベンチマークによって、その思いつきが正しいかどうか調べてみた。内容は簡単で、0〜10000000までの和を計算して出力するのにかかった時間を調べるというもの。念のため、最適化によって計算自体が消去されてしまわないように、計算した和を最後に出力している。bench1〜bench3はそれぞれ
- bench1: fromからtoまで、1ずつ増加させながら、関数f(型はInt => Unit)を呼び出すメソッドupto_1を使って計算を行う
- bench2: fromからtoまで、1ずつ増加させながら、IntAction(Int => Unitを継承している)のインスタンスのapply()を呼び出すメソッドupto_2を使って計算を行う
- bench3: whileを使って直接計算を行う
というものになっている。ポイントは、IntActionが(Int => Unit)を継承しつつ、applyを再宣言しているところで、このようにすることで、不要なboxing/unboxingを行わないバージョンのapplyが生成される。
実行環境は、以下の通り。
- CPU: AMD Athlon 64 3000+(2.0GHz)
- RAM: 1.5GB
- OS: Windows XP Professional(SP3)
- JVM: jdk1.6.0(client VM)
- Scala: Scala 2.7.5 final
abstract class IntAction extends (Int => Unit) { def apply(x: Int): Unit } def upto_1(from: Int, to: Int)(f: Int => Unit) { var i = from while(i <= to) { f(i); i += 1 } } def upto_2(from: Int, to: Int)(f: IntAction) { var i = from while(i <= to) { f(i); i += 1 } } val MAX = 10000000 object bench1 extends scala.testing.Benchmark { def run { var sum = 0L upto_1(0, MAX){i => sum += i } println("sum = " + sum) } } object bench2 extends scala.testing.Benchmark { def run { var sum = 0L upto_2(0, MAX)(new IntAction { def apply(i: Int) { sum += i; } }) println("sum = " + sum) } } object bench3 extends scala.testing.Benchmark { def run { var sum = 0L var i = 0 while(i <= MAX) { sum += i; i += 1 } println("sum = " + sum) } } println(bench1.runBenchmark(10)) println(bench2.runBenchmark(10)) println(bench3.runBenchmark(10))
その結果、以下の出力が得られた(sum = の部分は余分なので省略。リストの各要素が合計値を計算するのにかかった1回の時間を表している)。
(snip) List(406, 391, 391, 375, 391, 375, 391, 391, 360, 359) (snip) List(109, 93, 93, 94, 110, 94, 79, 94, 94, 94) (snip) List(31, 32, 32, 31, 31, 16, 15, 31, 31, 31)
見ればわかるように、IntActionを使ったbench2の結果(whileループを直に書いたbench3に比べて3倍程度遅い)は、bench1(bench3に比べて10倍程度遅い)に比べてかなり改善している。さらに、-serverオプションを付けて、server VMで実験してみると、以下の出力が得られた。server VMではbench2とbench3の差がほぼ無くなっているのが面白い。
(snip) List(360, 313, 296, 313, 313, 313, 313, 312, 437, 312) (snip) List(31, 16, 15, 31, 15, 16, 16, 31, 16, 15) (snip) List(31, 31, 15, 16, 16, 16, 16, 15, 94, 16)
補足:
upto_1に渡される無名関数について、scalac(2.7.5 final)では、次のような二つのapplyメソッドが生成されるが、upto_1の定義側では、後者(void apply(int))のapplyの存在を知り得ないため、無駄にboxing/unboxingを行う前者のバージョンを呼び出さざるを得ない。
public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokestatic #36; //Method scala/runtime/BoxesRunTime.unboxToInt:(Lja va/lang/Object;)I 5: invokevirtual #39; //Method apply:(I)V 8: getstatic #45; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime /BoxedUnit; 11: areturn public final void apply(int); Code: 0: aload_0 1: getfield #12; //Field sum$1:Lscala/runtime/LongRef; 4: aload_0 5: getfield #12; //Field sum$1:Lscala/runtime/LongRef; 8: getfield #53; //Field scala/runtime/LongRef.elem:J 11: iload_1 12: i2l 13: ladd 14: putfield #53; //Field scala/runtime/LongRef.elem:J 17: return
一方、upto_2に渡されるIntActionの無名サブクラスについても同様に、以下のような二つのapply()が生成されるが、upto_2の定義側では、渡されるのがIntActionのインスタンスであることがわかっているため、オーバーヘッドが少ない後者のapplyを呼び出すことができる。
public java.lang.Object apply(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokestatic #36; //Method scala/runtime/BoxesRunTime.unboxToInt:(Lja va/lang/Object;)I 5: invokevirtual #39; //Method apply:(I)V 8: getstatic #45; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime /BoxedUnit; 11: areturn public void apply(int); Code: 0: aload_0 1: getfield #12; //Field sum$2:Lscala/runtime/LongRef; 4: aload_0 5: getfield #12; //Field sum$2:Lscala/runtime/LongRef; 8: getfield #53; //Field scala/runtime/LongRef.elem:J 11: iload_1 12: i2l 13: ladd 14: putfield #53; //Field scala/runtime/LongRef.elem:J 17: return }
追記:
間抜けなことに、upto_1とupto_2の定義で、iをfromではなく0で初期化していたので修正。結果は変わらないはずだけど、念のため結果もベンチマークを取り直したものに差し替え。