突然发现暂停函数的递归调用需要更多时间然后调用相同的函数但没有suspend
修饰符,所以请考虑下面的代码片段(基本的斐波纳契数列计算):
suspend fun asyncFibonacci(n: Int): Long = when {
n <= -2 -> asyncFibonacci(n + 2) - asyncFibonacci(n + 1)
n == -1 -> 1
n == 0 -> 0
n == 1 -> 1
n >= 2 -> asyncFibonacci(n - 1) + asyncFibonacci(n - 2)
else -> throw IllegalArgumentException()
}
如果我调用此函数并使用以下代码测量其执行时间:
fun main(args: Array<String>) {
val totalElapsedTime = measureTimeMillis {
val nFibonacci = 40
val deferredFirstResult: Deferred<Long> = async {
asyncProfile("fibonacci") { asyncFibonacci(nFibonacci) } as Long
}
val deferredSecondResult: Deferred<Long> = async {
asyncProfile("fibonacci") { asyncFibonacci(nFibonacci) } as Long
}
val firstResult: Long = runBlocking { deferredFirstResult.await() }
val secondResult: Long = runBlocking { deferredSecondResult.await() }
val superSum = secondResult + firstResult
println("${thread()} - Sum of two $nFibonacci'th fibonacci numbers: $superSum")
}
println("${thread()} - Total elapsed time: $totalElapsedTime millis")
}
我观察到了进一步的结果:
commonPool-worker-2:fibonacci - Start calculation...
commonPool-worker-1:fibonacci - Start calculation...
commonPool-worker-2:fibonacci - Finish calculation...
commonPool-worker-2:fibonacci - Elapsed time: 7704 millis
commonPool-worker-1:fibonacci - Finish calculation...
commonPool-worker-1:fibonacci - Elapsed time: 7741 millis
main - Sum of two 40'th fibonacci numbers: 204668310
main - Total elapsed time: 7816 millis
但如果我从suspend
函数中删除asyncFibonacci
修饰符,我会得到以下结果:
commonPool-worker-2:fibonacci - Start calculation...
commonPool-worker-1:fibonacci - Start calculation...
commonPool-worker-1:fibonacci - Finish calculation...
commonPool-worker-1:fibonacci - Elapsed time: 1179 millis
commonPool-worker-2:fibonacci - Finish calculation...
commonPool-worker-2:fibonacci - Elapsed time: 1201 millis
main - Sum of two 40'th fibonacci numbers: 204668310
main - Total elapsed time: 1250 millis
我知道用tailrec
重写这样一个函数会更好,它会增加执行时间apx。几乎是100次,但无论如何,这个suspend
关键词会将执行速度从1秒降低到8秒?
用suspend
标记递归函数是完全愚蠢的想法吗?
答案 0 :(得分:4)
作为介绍性评论,您的测试代码设置过于复杂。这个更简单的代码在强调suspend fun
递归方面达到了相同的目的:
fun main(args: Array<String>) {
launch(Unconfined) {
val nFibonacci = 37
var sum = 0L
(1..1_000).forEach {
val took = measureTimeMillis {
sum += suspendFibonacci(nFibonacci)
}
println("Sum is $sum, took $took ms")
}
}
}
suspend fun suspendFibonacci(n: Int): Long {
return when {
n >= 2 -> suspendFibonacci(n - 1) + suspendFibonacci(n - 2)
n == 0 -> 0
n == 1 -> 1
else -> throw IllegalArgumentException()
}
}
我尝试通过编写一个简单的函数来重现它的性能,该函数近似于suspend
函数必须做的事情以实现可挂起性:
val COROUTINE_SUSPENDED = Any()
fun fakeSuspendFibonacci(n: Int, inCont: Continuation<Unit>): Any? {
val cont = if (inCont is MyCont && inCont.label and Integer.MIN_VALUE != 0) {
inCont.label -= Integer.MIN_VALUE
inCont
} else MyCont(inCont)
val suspended = COROUTINE_SUSPENDED
loop@ while (true) {
when (cont.label) {
0 -> {
when {
n >= 2 -> {
cont.n = n
cont.label = 1
val f1 = fakeSuspendFibonacci(n - 1, cont)!!
if (f1 === suspended) {
return f1
}
cont.data = f1
continue@loop
}
n == 1 || n == 0 -> return n.toLong()
else -> throw IllegalArgumentException("Negative input not allowed")
}
}
1 -> {
cont.label = 2
cont.f1 = cont.data as Long
val f2 = fakeSuspendFibonacci(cont.n - 2, cont)!!
if (f2 === suspended) {
return f2
}
cont.data = f2
continue@loop
}
2 -> {
val f2 = cont.data as Long
return cont.f1 + f2
}
else -> throw AssertionError("Invalid continuation label ${cont.label}")
}
}
}
class MyCont(val completion: Continuation<Unit>) : Continuation<Unit> {
var label = 0
var data: Any? = null
var n: Int = 0
var f1: Long = 0
override val context: CoroutineContext get() = TODO("not implemented")
override fun resumeWithException(exception: Throwable) = TODO("not implemented")
override fun resume(value: Unit) = TODO("not implemented")
}
你必须用
调用这个sum += fakeSuspendFibonacci(nFibonacci, InitialCont()) as Long
其中InitialCont
是
class InitialCont : Continuation<Unit> {
override val context: CoroutineContext get() = TODO("not implemented")
override fun resumeWithException(exception: Throwable) = TODO("not implemented")
override fun resume(value: Unit) = TODO("not implemented")
}
基本上,要编译suspend fun
,编译器必须将其主体转换为状态机。每次调用还必须创建一个对象来保存机器的状态。当你恢复时,状态对象告诉去哪个状态处理程序。以上仍然不是全部,真正的代码甚至更复杂。
在intepreted模式(java -Xint
)中,我获得与实际suspend fun
几乎相同的性能,并且它比启用JIT的实际速度低两倍。相比之下,&#34;直接&#34;功能实现速度快10倍左右。这意味着所显示的代码解释了可暂停性的很大一部分开销。
答案 1 :(得分:2)
问题在于从suspend
函数生成的Java字节码。虽然非suspend
函数只是像我们期望的那样生成字节码:
public static final long asyncFibonacci(int n) {
long var10000;
if (n <= -2) {
var10000 = asyncFibonacci(n + 2) - asyncFibonacci(n + 1);
} else if (n == -1) {
var10000 = 1L;
} else if (n == 0) {
var10000 = 0L;
} else if (n == 1) {
var10000 = 1L;
} else {
if (n < 2) {
throw (Throwable)(new IllegalArgumentException());
}
var10000 = asyncFibonacci(n - 1) + asyncFibonacci(n - 2);
}
return var10000;
}
当你添加suspend关键字时,反编译的Java源代码是165行 - 所以要大得多。您可以通过转到工具 - &gt;来查看IntelliJ中的字节码和反编译的Java代码。 Kotlin - &gt; 显示Kotlin字节码(然后单击页面顶部的反编译)。虽然要告诉Kotlin编译器在函数中究竟做了什么并不容易,但看起来它正在进行大量的协程状态检查 - 考虑到协程可以随时挂起,哪种方式有意义。
因此,作为结论,我会说每个suspend
方法调用都比非suspend
调用重得多。这不仅适用于递归函数,但可能对它们造成最坏的结果。
使用suspend标记递归函数是完全愚蠢的想法吗?
除非你有充分的理由这样做 - 是的