🤔

コルーチンをまとめて開始するより、バラバラに開始した方が速い

2024/02/04に公開
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() {
    // これが一番遅い
    val time = measureTimeMillis {
        runBlocking {
            coroutineScope {
                val defer = async { delayOneSecond() }
                defer.await()
                val defer2 = async { delayOneSecond() }
                defer2.await()
            }
        }
    }
    println(time)

    // なぜか遅い
    val time3 = measureTimeMillis {
        runBlocking {
            coroutineScope {
                val deferreds = listOf(
                    async { delayOneSecond() },
                    async { delayOneSecond() }
                )
                deferreds.awaitAll()
            }
        }
    }
    println(time3)

    val time2 = measureTimeMillis {
        runBlocking {
            coroutineScope {
                val defer = async { delayOneSecond() }
                val defer2 = async { delayOneSecond() }
                defer.await()
                defer2.await()
            }
        }
    }
    println(time2)
}

suspend fun delayOneSecond() {
    delay(1000)
}

こんな感じのKotlinコードがあった場合に、毎回大体これくらいの実行時間(ms)になります。
これはandroidのコードじゃなくて、Kotlin/JVMのコードになります。

2040
1034
1009

一番最初が一番遅いのはわかるとして、2番目awaitAll()のパターンが遅いのはよくわかりません。
何回やっても同様の結果になります。
ちなみにですが、2番目と3番目のコードは、戻り値が特に必要ない場合は、coroutineScopeが内部のコルーチンが全て完了するまでは、次に処理が進まないのでawaitが不要になります。
ということは、listOfで囲ってるか囲ってないかで、速度が違いそうです。

ただ、そんなに処理時間が違わないので、綺麗に描ける方を使いたくなりますよね。この辺りはチーム内で事前に話し合っておかないと揉めるポイントになりそうです。
また、時間があるときに、listOfの部分を追ってみたいと思います。
※単にlistOfの処理が遅いだけかもしれませんが。。。

ここでなんとなく、JavaScriptではどうなるんだろうと思いました。

// sleep関数はいまだにないそうで。
function sleep(msec) {
   return new Promise(function(resolve) {
      setTimeout(function() {resolve()}, msec);
   })
}

async function asyncCall() {
  const t0 = performance.now();
  await Promise.all([delayOneSecond(), delayOneSecond()])
  const t1 = performance.now();
  console.log(`took ${t1 - t0} milliseconds.`);

  const t2 = performance.now();
  p1 = delayOneSecond()
  p2 = delayOneSecond()
  await p1
  await p2
  const t3 = performance.now();
  console.log(`took ${t3 - t2} milliseconds.`);
}

async function delayOneSecond(log) {
  await sleep(1000);
}

asyncCall();

3回の実行結果はこんな感じです。

> "took 1005.6999998092651 milliseconds."
> "took 1002.7000000476837 milliseconds."

> "took 1005.7000000476837 milliseconds."
> "took 1006.2999999523163 milliseconds."

> "took 1001 milliseconds."
> "took 1004.5 milliseconds."

早かったり遅かったりします。これは、JavaScriptを普段書いてるメンバーなどがいるとさらに揉めそうです。
結論は特にないですが、awaitAllもしくは、Promise.allを使う方が、コードの意図がよくわかりそうです。

Kotlinだと、20msくらい違って、端末によっては、もしかするともっと差がつく可能性があるので、臨機応変な対応が必要そうです。

サンプルコード(Kotlinのみ)

https://github.com/na8esin/sample-kotlin-jvm/blob/6e322509b8319771723974cbb855fb2064bbbf59/src/main/kotlin/await/AwaitAllFirstNoHeavy.kt

参考

https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ja
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

Discussion