问题
I forge a simple example to check the @inline annotation behavior:
import scala.annotation.tailrec
object InlineTest extends App {
@inline
private def corec(x : Int) : Int = rec(x - 1)
@tailrec
private def rec(x : Int) : Int =
if (x < 3) x else {
if (x % 3 == 0)
corec(x-1)
else
rec(x-4)
}
@tailrec
private def rec1(x : Int) : Int =
if (x < 3) x else {
if (x % 3 == 0) {
val arg = x - 1
rec1(arg - 1)
} else
rec1(x-4)
}
Console.println( rec(args(0).toInt) )
}
This example compiled without warnings, both tailrec annotations were in effect as I thought. But when I actually execute the code, it gives me stackoverflow exception. This means that the tail recursion optimization failed due to inline method being not so inlined.
I have control function rec1 that differs from the original only with "inline" transformation performed manually. And as this function works well as expected avoiding stackoverflow exception with tail recursion.
What prevents the annotated method from being inlined?
回答1:
This won't work, indeed. The treatment of @inline is applied way later than the treatment of tail calls, preventing the optimization you're hoping for.
During the so-called "tailcalls" phase of the compiler, the compiler tries to transform methods so that tail-recursive calls are removed and replaced by loops. Note that only calls within the same function can be eliminated in this way. So, here, the compiler will be able to transform the function rec into something more or less equivalent to the following:
@tailrec
private def rec(x0: Int) : Int =
var x: Int = x0
while (true) {
if (x < 3) x else {
if (x % 3 == 0)
return corec(x-1)
else {
x = x-4
continue
}
}
}
Note that the call to corec is not eliminated, because you're calling another method. At that time in the compiler, @inline annotations are not even looked at.
But why doesn't the compiler warn you that it cannot eliminate the call, since you've put @tailrec? Because it only checks that it is actually able to replace all calls to the current method. It doesn't care whether another method in the program calls rec (even if that method, in this case corec, is itself called by rec).
So you get no warning, but still the called to corec is not tail-call eliminated.
When you get to the treatment of @inline later in the compiler pipeline, and assuming it does choose to follow your advice to inline corec, it will modify again rec to inline the body of corec, which gives the following:
@tailrec
private def rec(x0: Int) : Int =
var x: Int = x0
while (true) {
if (x < 3) x else {
if (x % 3 == 0)
return rec((x-1) - 1)
else {
x = x-4
continue
}
}
}
Never mind then that this creates a new tail-recursive call. It's too late. It won't be eliminated anymore. Hence, the stack overflow.
You can use the TailCalls object and its methods to turn mutually tail-recursive methods into loops. See details in the API.
回答2:
Note that there is a proposal in progress (scala PR 567) to introduce an actual inline keyword. (Sept. 2016)
See "SIP NN: Inline Definitions and Meta Expressions (Public Draft)"
It will work for:
concrete value definitions, e.g.
inline val x = 4
concrete methods, e.g.
inline def square(x: Double) = x * x
parameters of inline methods, e.g.
inline def pow(b: Double, inline n: Int): Double = {
if (n == 0) 1
else pow(b, n - 1)
}
Value and method definitions labeled inline are effectively final; they cannot be overridden.
Inline members also never override other members. Instead, every inline member becomes an overloaded alternative of all other members with the same name.Inline definitions only exist at compile time; no storage is allocated for them in object layout and no code is generated for them in object method tables.
That means that it is OK to have an inline member that has the same type erasure as some other member with the same name.
来源:https://stackoverflow.com/questions/25582515/make-method-actually-inline