🧵

内部実装から理解するSwift Concurrency — async 関数はコンパイル時にどう変換されるのか

に公開

こちらは「内部実装から理解するSwift Concurrency」シリーズの、1つ目の記事です。

今回は、Swiftのasync関数がコンパイル時にどのように変換されるのかを深ぼることで、Swift Concurrencyにおける中断・再開の仕組みを明らかにします。

この記事から学べること

  • Swiftコードがコンパイルされる際の基本的な流れが分かる
  • async関数から生成されるSwift Intermediate Language (SIL) の雰囲気が掴める
  • async関数においてhop_to_executorというSILの命令が果たす役割が分かる

Swiftコードがコンパイルされる流れ

async関数が中断・再開される仕組みを内部実装から理解するためには、コンパイル時にどのような変換が行われるのかを知る必要があります。

Swiftコードのコンパイル時には、抽象構文木 (AST)、Swift Intermediate Language (SIL)、LLVM IRを経て、最終的にオブジェクトファイル (*.o) が生成されます[1][2]

SILとは、Swiftコードのコンパイル時に生成される中間表現 (IR) の一種です。SILには2種類あり、まずSILGenによってASTから「raw SIL」と呼ばれるIRが生成されます。
その後、「raw SIL」は、SILOptimizerによって「canonical SIL」と呼ばれる、さまざまな最適化やエラーチェックが行われた状態のIRへと変換されます。

その後、「canonical SIL」は、IRGenによって、LLVM IRへと変換されます。
LLVM IRは、Swift言語に依らず、C++やRust等でも使用されている中間表現です。最終的に、LLVM IRがLLVMによってオブジェクトファイルへと変換されます。

ここでは、async関数から生成されるSILを解読していきます。

async関数のSILへの変換

以下のasync関数をSILへと変換します。

import Foundation

private func getIntAsync(x: Int) async -> Int {
    return x
}

public func simpleAsyncFunc() async -> Int {
    let a = await getIntAsync(x: 1)
    let b = await getIntAsync(x: 2)
    return a + b
}

SILへの変換は、以下のようなコマンドで行います。
ファイル名がSimpleAsyncFunc.swiftであるという想定です。
ここでは、最適化やエラーチェックが行われる前の「raw SIL」を生成しています。

swiftc -emit-silgen SimpleAsyncFunc.swift -o SimpleAsyncFunc.sil

Swiftのバージョンは以下の通りです。

% swift --version
swift-driver version: 1.115 Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)
Target: arm64-apple-macosx15.0

コマンドを実行した結果、以下のようなSILが生成されます。

async関数のSILの解読

simpleAsyncFuncのSILについて、重要な部分にフォーカスして解読していきます。
ここで重要となるのは、「hop_to_executor」という命令です。

hop_to_executor %0 : $Optional<Builtin.Executor>

simpleAsyncFuncの呼び出し直後、およびgetIntAsyncの完了後に、hop_to_executorが呼ばれていることが分かります。

Swiftリポジトリのdocs/SIL/instructions.md内に、hop_to_executorも含めたSILの命令一覧のドキュメンテーションが存在します。ここから、hop_to_executorの説明を抜粋します[3]

sil-instruction ::= 'hop_to_executor' sil-operand

hop_to_executor %0 : $T

// $T must be Builtin.Executor or conform to the Actor protocol

Ensures that all instructions, which need to run on the actor's
executor actually run on that executor. This instruction can only be
used inside an @async function.

Checks if the current executor is the one which is bound to the operand
actor. If not, begins a suspension point and enqueues the continuation
to the executor which is bound to the operand actor.

ざっくり言うと、hop_to_executorは「この先の処理を、指定したExecutor上で実行するようにスレッド (実行コンテキスト) を切り替えるSIL命令」です。

現在のExecutorが、指定されたExecutorと一致している場合には、そのまま処理を継続します。
一致していない場合には、処理を一時中断し、指定されたExecutorに対して処理をenqueue します。


試しに、simpleMainAsyncFunc@MainActorで実行するよう変更してみます。

import Foundation

private func getIntAsync(x: Int) async -> Int {
    return x
}

@MainActor
public func simpleMainAsyncFunc() async -> Int {
    let a = await getIntAsync(x: 1)
    let b = await getIntAsync(x: 2)
    return a + b
}

すると、以下のように、hop_to_executorMainActorでの実行が指定されることが分かります。

hop_to_executor %3 : $MainActor 


まとめると、async関数の実行・再開時には、hop_to_executor命令によって、実行するスレッドの切り替えが必要に応じて行われることが分かりました。

ただし、hop_to_executorの内部で行われていること、すなわちどのように別のスレッドに切り替わり、そこで処理が非同期的に継続されるのかは分かっていません。
そこで、次の記事では、「hop_to_executorの内部実装」に関して、深掘りしていきます。

脚注
  1. Swift Compiler入門: https://kitasuke.booth.pm/items/1865964 ↩︎

  2. SIL in the Swift Compiler: https://github.com/swiftlang/swift/blob/main/docs/SIL/SIL.md#sil-in-the-swift-compiler ↩︎

  3. hop_to_executor: https://github.com/swiftlang/swift/blob/d1227fb693aa74181c4bfe191cb898a0a028d4be/docs/SIL/Instructions.md?plain=1#L276 ↩︎

Discussion