コトリンとテール再帰
1. 前書き
一部のアルゴリズムは、再帰的な方法で実装された場合に最適に機能します。つまり、計算が同じ計算のより単純な形式に基づいている場合です。
ほとんどのプログラミング言語では、再帰に関連するスタックオーバーフローのリスクがあります。 戻ることなく一度に実行できるネストされたメソッド呼び出しの数には制限があります。
これが問題になる場合は、代わりに従来のループを使用して、アルゴリズムを命令的な方法で書き直すことができます。 Tail recursion is a technique where the compiler can rewrite a recursive method in an imperative manner、特定のルールが満たされていると仮定します。
2. Kotlinでの末尾再帰のルール
末尾再帰を使用してKotlinで関数を実装するには、従うべき1つのルールがあります:the recursive call must be the very last call of the method。 このルールは見かけほど従うのが簡単ではありません。 たとえば、要因の例を使用すると、これは次のように実装されます。
このルールは見かけほど従うのが簡単ではありません。 たとえば、要因の例を使用すると、これは次のように実装されます。
fun recursiveFactorial(n: Long) : Long {
return if (n <= 1) {
n
} else {
n * recursiveFactorial(n - 1)
}
}
これは完璧に機能します。 ただし、末尾再帰の対象にはなりません。
分解すると、上記の関数は次のことを行います。
-
nが⇐1の場合、nを返します
-
accum = recursiveFactorial(n – 1)を計算する
-
n * accumを返す
そのように書かれているので、再帰呼び出しは関数の最後の呼び出しではないことがわかります。
3. 末尾再帰としての階乗の実装
代わりに、末尾再帰を使用して階乗関数を実装するには、計算を実行する場所を変更するためにそれを再処理する必要があります。 We need to ensure that the multiplication is done before the recursive call, not after。 これは、部分的な結果をパラメーターとして渡すことで最も簡単に実行できます。
fun factorial(n: Long, accum: Long = 1): Long {
val soFar = n * accum
return if (n <= 1) {
soFar
} else {
factorial(n - 1, soFar)
}
}
これは次のように分類できます。
-
soFar = n * accumを計算する
-
n⇐1の場合、soFarを返します
-
n – 1とsoFarを渡して、factorial関数を呼び出します
今回は、再帰呼び出しがプロセスの最後のステップです。
正しい形式の再帰関数ができたら、we instruct Kotlin to consider it for tail recursion by use of the tailrec keyword。 これにより、コンパイラーは関数をループとして書き直すことができる場合に、ループとして書き直すことが許可されます。 このキーワードは関数自体に適用されるため、次のようになります。
tailrec fun factorial(n: Long, accum: Long = 1): Long
4. 階乗のコンパイル出力
これの目標は、スタックオーバーフローの問題を回避するために、命令的な方法で実行される再帰的なコードを記述することです。 上記の関数を逆コンパイルすると、コンパイラによって生成された結果が実際に必須であることがわかります。
public final long factorial(long n, long accum) {
while(n > (long) 1) {
long var10000 = n - (long)1;
accum = n * accum;
n = var10000;
}
return n * accum;
}
これがどのように機能するかをすぐに確認でき、このコードにはスタックオーバーフローのリスクはまったくありません。
5. パフォーマンスの向上
この最適化を使用することで、パフォーマンスが向上するほか、安全性が向上する場合があります。 これらの利点は、再帰の深さや計算の複雑さなど、他のいくつかの要因に依存します。
改善点は、メソッド呼び出しが単にループするよりもコストがかかるという事実から来ています。
再び階乗の例を使用して、実行と比較にかかる時間を測定できます。
-
末尾再帰なしでfactorial(50)を1,000,000回計算するには、約70ミリ秒かかります
-
末尾再帰を使用してfactorial(50)を1,000,000回計算するには、約45ミリ秒かかります
単純なベンチマークを使用すると、36%の速度向上が得られました。これは、コンパイラーが実装を再作業できるようにするためだけに重要です。
これらの測定値は、単純な関数の非常に単純なベンチマークからのものであることに注意してください。 実際のパフォーマンスの変化は状況に応じて異なるため、決定を行う前に常に測定する必要があります
6. 概要
一部のシナリオでは、末尾再帰により、安全性とパフォーマンスの両方に関して、コードにいくつかの重要な利点がもたらされます。 Kotlinのコンパイラはこれを自動的に実行できます。適切なキーワードを使用するだけです。