クラスメンバーメソッドの末尾再帰最適化の制限
PrgInScalaの8.9では,テール再帰(メソッドの再帰呼び出しはメソッド体の最後)メソッドScalaコンパイラがバイトコードをループに最適化し,性能を向上させることが述べられている.しかし、本の例を試してみると、想像していた最適化は起こらなかった.次はテストコードです.
以下に実行結果を示すと,再帰呼び出しの方式はループ方式よりもかなり遅く,回数が多ければ多いほど遅い幅が大きくなる傾向が見られる.
次にjavapでApproxクラスapproximateメソッドのバイトコードを見て、テール再帰が最適化されているかどうかを確認します.
バイトコードの18行でinvokevirtual命令でapproximateメソッドを呼び出すと,これは再帰呼び出しであり,コンパイラはテール再帰最適化を行っていないことがわかる.
テール再帰最適化を行わなかったものは何もありませんか?コードは本に書いてあるのと全く同じですね.Googleはちょっとして、原因を知って、もとはapproximate方法はfinal修飾子をプラスしていません.このapproximateメソッドは布団クラスoverrideの可能性があるため、コンパイラはそれをテール再帰的に最適化することはできません.次にapproximateメソッドの前にfinalを付けて、次のコードになります.
実行結果は以下の通りで,今回のテール再帰とループの2つの方式にかかる時間はほとんど差がない.
バイトコードをもう一度見てください.
もとの
18: invokevirtual #30;//Method approximate:(D)D
21: dreturn
今は
15: dstore_1
16: goto 0
すなわち,バイトコード階層で元の再帰の代わりにループを用いることで,速度は自然にコード階層のループ実現に匹敵する.
原書では、Scalaは、直接再帰的で間接的ではなく、関数オブジェクト呼び出しによって実現される再帰的ではないなど、末尾再帰の最適化に多くの制限があることも述べている.以上より,本明細書で述べたクラスメンバーメソッドはfinalでなければならないことも制限条件の一つである.ちなみにobjectで定義されているメソッドであればfinal修飾子を付ける必要はありません.それはobjectのすべてのメソッドがデフォルトでfinalであるためです.
package fineqtbull;
class Approx {
def isGoodEnough(guess:Double):Boolean =
if (guess < 1) true
else false
def improve(guess:Double): Double = guess - 1
def approximate(guess:Double):Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))
def approximateLoop(initialGuess:Double):Double = {
var guess = initialGuess
while (!isGoodEnough(guess))
guess = improve(guess)
guess
}
}
object ApproxMain {
val app = new Approx()
def main(args:Array[String]) = {
println(System.getProperty("java.class.path"))
recursive(1)
iterate(1)
recursive(10)
iterate(10)
recursive(100)
iterate(100)
}
def recursive(n:Int) = {
val start = java.lang.System.currentTimeMillis()
for (i <- 0 to 10000000) {
app.approximate(n)
}
println(java.lang.System.currentTimeMillis() - start)
}
def iterate(n:Int) = {
val start = java.lang.System.currentTimeMillis()
for (i <- 0 to 10000000) {
app.approximateLoop(n)
}
println(java.lang.System.currentTimeMillis() - start)
}
}
以下に実行結果を示すと,再帰呼び出しの方式はループ方式よりもかなり遅く,回数が多ければ多いほど遅い幅が大きくなる傾向が見られる.
922
969
2406
2032
13578
8047
次にjavapでApproxクラスapproximateメソッドのバイトコードを見て、テール再帰が最適化されているかどうかを確認します.
public double approximate(double);
Code:
Stack=4, Locals=3, Args_size=2
0: aload_0
1: dload_1
2: invokevirtual #18; //Method isGoodEnough:(D)Z
5: ifeq 12
8: dload_1
9: goto 21
12: aload_0
13: aload_0
14: dload_1
15: invokevirtual #21; //Method improve:(D)D
18: invokevirtual #30; //Method approximate:(D)D
21: dreturn
LineNumberTable:
line 8: 0
line 9: 12
line 8: 21
バイトコードの18行でinvokevirtual命令でapproximateメソッドを呼び出すと,これは再帰呼び出しであり,コンパイラはテール再帰最適化を行っていないことがわかる.
テール再帰最適化を行わなかったものは何もありませんか?コードは本に書いてあるのと全く同じですね.Googleはちょっとして、原因を知って、もとはapproximate方法はfinal修飾子をプラスしていません.このapproximateメソッドは布団クラスoverrideの可能性があるため、コンパイラはそれをテール再帰的に最適化することはできません.次にapproximateメソッドの前にfinalを付けて、次のコードになります.
class Approx {
...
final def approximate(guess:Double):Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))
...
}
...
実行結果は以下の通りで,今回のテール再帰とループの2つの方式にかかる時間はほとんど差がない.
937
1000
1969
2109
8219
8328
バイトコードをもう一度見てください.
public final double approximate(double);
Code:
Stack=3, Locals=4, Args_size=2
0: aload_0
1: dload_1
2: invokevirtual #18; //Method isGoodEnough:(D)Z
5: ifeq 10
8: dload_1
9: dreturn
10: aload_0
11: dload_1
12: invokevirtual #21; //Method improve:(D)D
15: dstore_1
16: goto 0
LineNumberTable:
line 8: 0
line 7: 9
line 9: 10
もとの
18: invokevirtual #30;//Method approximate:(D)D
21: dreturn
今は
15: dstore_1
16: goto 0
すなわち,バイトコード階層で元の再帰の代わりにループを用いることで,速度は自然にコード階層のループ実現に匹敵する.
原書では、Scalaは、直接再帰的で間接的ではなく、関数オブジェクト呼び出しによって実現される再帰的ではないなど、末尾再帰の最適化に多くの制限があることも述べている.以上より,本明細書で述べたクラスメンバーメソッドはfinalでなければならないことも制限条件の一つである.ちなみにobjectで定義されているメソッドであればfinal修飾子を付ける必要はありません.それはobjectのすべてのメソッドがデフォルトでfinalであるためです.