The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

Swift Concurrencyのパフォーマンス

Swift Concurrencyは並列プログラミングの書きやすさや便利さがよくなっただけでなくパフォーマンスも改善されています。

これまでの問題

これまでのGCDを使った並列処理では、CPU Coreに対して一つスレッドを割り当てそれをスレッドプールにプールして再利用するのでnon blockingな処理を並列処理する分にはスレッドの切り替えが発生しないためパフォーマンスには問題ありません。

しかし、スレッドをブロックするような処理する場合はスレッドが止まってしまい、止まっている間CPUが無駄にならないように別の処理を実行するためには、新たにスレッドを生成してそのスレッドに切り替えが発生します。スレッドを切り替えるには、今あるスレッドの状態(レジスタとスタック)を退避し、次に実行するスレッドの状態を復元する必要があります。これらスレッドの生成や状態の退避と復元がコンテキストスイッチで、これを行うにはもちろんコストがかかるのでパフォーマンスに影響します。

また、スレッドをブロックする処理が大量に発生し、スレッドが枯渇(スレッド生成数の限界を超える)する可能性もあります。これをThread Explosionと呼びます。 Thread Explosionが発生するとそれ以上スレッドが生成できなくなるだけでなく、dead lockを起こす可能性があります。(ちなみにlimitは64らしい)

Thread Explosionによるdead lock

WWDC2015のビデオの例です。 f:id:yanamura:20220223235954p:plain

①main threadからmain threadに対してdispatch_syncするタスクを大量にdispatch_asyncしてスレッドを枯渇させます。

②つぎに、dispatch_syncで呼ばれた処理内でserial queueにdispatch_asyncするとそのserial queueはスレッド不足のためスレッドの空き待ちになります。そのあと同じserial queueにdispatch_syncするとスレッドのスレッドの空き待ちが解消するまでdispatch_syncの処理は実行されないです。このdispatch_syncの処理が終わらないとmain threadはブロックされ、更にmain threadがブロックされると①のdispatch_syncも終わらないのでスレッドが開放されずdead lockとなります。

これまでの問題のまとめ

  • スレッドをブロックするような処理があるとコンテキストスイッチが発生してパフォーマンスが落ちる
  • スレッドが枯渇するとdead lockを起こすおそれがある

Swift Concurrencyの場合

Swift Concurrencyでは、CPUに対して1スレッドが割り当てられ、スレッドの切り替えが起こらないようになっています。

どうやっているかというとGCDのようにスレッドを内包して使いやすくしているのではなく、continuationという軽量スレッド的な仕組みを使っているようです。 (詳細はこちらのSwift concurrency:Behind the scenes

asyncが呼ばれてsuspendされた際に元の状態に戻すための情報(stack上に持っているlocal変数など)をheapに保存し、resumeするときにheapからstackに戻すことで再開されているようです。

f:id:yanamura:20220309000016p:plain

スレッドとの主な違いは、

  • スレッドは生成時にスタックサイズ分のメモリの確保が必要だが、continuationの場合は必要な分だけ確保?
  • スレッドの場合は切替時にレジスタの退避復帰が必要だが、continuationの場合はheapからstackに戻すだけ

ということなのかなと思われます。(違ってたらご指摘いただけると幸いです)

結局のところはスレッドであろうとcontinuationであろうと大量生成するとメモリは食うし切り替えコストは発生します。ですので、いくらSwift Concurrencyで改善されたからといって大量に並列処理を行っても速くなるとは限りません。

このようにSwift Concurrencyではこれまでよりパフォーマンスは改善されていますが、依然として並列処理にはメモリや切り替えコストは発生するのでその点は意識して使っていく必要はありますね