Open78

Swift: 呼び出し規約を調べる

kabeyakabeya

「Swiftポインタ入門」という本を書いています(書いてる最中です)。
そのなかで「関数を呼び出す場合、引数は呼び出し元の変数から呼び出し先の変数にコピーされます」というように書いたのですが、本当かな?ということで改めて調べ直そうと思います。

  • Swiftで関数を呼ぶ際、どのようなことが起きているのか
  • 型や関数で違いがあるか

これらの点について、コンパイラの出力を見て確認することにします。

最初は全部調べきってから記事にしようかと思っていたんです。
ですが、間違って理解している箇所もあって戻ったりしているので、その調査とかやり直しの過程込みで徐々に書いていった方がいいのかなという気がしてきました。
まとまったら記事にします。

kabeyakabeya

Swiftの呼び出し規約について

Swiftの呼び出し規約は、The Swift Calling Conventionに記述があります。

上記のドキュメントによれば「Swiftの呼び出し規約」は、大きく3レベルに分けられます。

  1. 高級言語レベルでの呼び出し規約:主に参照による引数渡し vs 値による引数渡しの話
  2. SIL(Swift Intermediate Languate)レベルでの呼び出し規約:引数や返値の所有権移転、retain/release/コピーに関するもの
  3. マシン語(≒アセンブラ言語)レベルでの呼び出し規約:レジスタやメモリの使用方法に関するもの

Swift Compiler: Compiler Architectureによれば、Swiftのソースコードは以下のようなステップでマシン語になります。

No. 処理 生成物
1 - Swiftソースコード
2 構文解析 parsed AST
3 文脈・意味解析 type-checked AST
4 C/C++/Objective-Cインポート +(imported AST)
5 SIL生成 raw SIL
6 データフロー解析 canonical SIL
7 最適化 (canonical SIL)※上記ドキュメントでは言及ありません
8 LLVM IR生成 LLVM IR
9 オブジェクト生成 マシン語

今回はSwift自体の呼び出し規約の種類ごとに、表のNo.6(といういか7?)の部分の出力(canonical SIL)と、表のNo.9の部分の出力(のアセンブラ)を確認します。

kabeyakabeya

出力の確認方法

SILの確認はswiftc -emit-sil ソースコード.swift -o SILファイル名.silで出力したものを確認します。

アセンブラは同様にswiftc -target x86_64-apple-macos13 -emit-assembly ソースコード.swift -o アセンブラファイル名.asmで出力できます[1]
この場合はAT&T形式の出力です。ちょっと見にくい気がします。

もしくはswiftc -target x86_64-apple-macos13 -emit-object ソースコード.swift -o オブジェクトファイル名.o ; objdump -d -M intel オブジェクトファイル名.oでも出力できます。
この場合はIntel形式の出力です。自分としてはこちらが好みなのですが、シンボル名が分かりにくいという問題があります。

というわけで、「Compiler Explorer」というサイトの出力で確認します。
このサイトは、ソースコードを貼り付けると、それをコンパイルしてアセンブラを出力してくれます。
様々な言語に対応しています。出力も色々なハードウェアに対応しています(が、Swiftに関してはx86_64しか対応していません)。

今回はx86で確認しますが、原則、ARM64も同じ処理をしているはずです(ただし可変引数の呼び出し規約はx86_64とarm64で違う、ということです[2])。

脚注
  1. -target x86_64-apple-macos13がないと、Apple Silicon Macではarm64のアセンブラが出力されます ↩︎

  2. Addressing Architectural Differences in Your macOS Code (https://developer.apple.com/documentation/apple-silicon/addressing-architectural-differences-in-your-macos-code) ↩︎

kabeyakabeya

高級言語レベルでの呼び出し規約

「Swiftの呼び出し規約は、The Swift Calling Conventionに記述があります」とは書きましたが、実際、これには仕様がほとんど書かれていません(結論もあるんだかないんだか分からないような書き方ですし)。

現在の最新言語仕様が統一的に書かれているドキュメントが見当たらないような気がします(探せていないだけかも知れませんが)。

だからこそ、ここで調べようとしています。
調査開始時点で気付いている点は以下です。

  • 引数の渡しかた
    • 参照による引数渡しと値による引数渡しがある
    • 引数ごとに渡し方は異なる
    • inout/borrowing/consumingのパラメータ修飾子があり動きが変わる
    • 参照型と値型で動きが異なる
    • Copyableな型と~Copyableな型で制約が変わる
    • letvarで引数の渡し方が異なる
    • 「イニシャライザ、プロパティセッタ」と「それ以外の関数」で動きが変わる
    • 引数にはSwift独自のエイリアシングルールがある(今回は触れません)
  • 返値の受け取りかた
    • 原則、値による返値渡し
    • 返値がない場合も、空のタプルが返ってきていて、それを受け取ることができる
  • その他
    • 関数にはmutating/consuming/borrowingの修飾子を付けることができ、いずれもselfに関する制約が変わる(今回は触れません)

今回は、返値は深追いしないことにします。必要があれば多少触れるかも知れません。

kabeyakabeya

SILレベルの呼び出し規約

SILレベルの呼び出し規約は、Swift Intermediate Language (SIL)に記述があります。

上記ドキュメントには、以下の呼び出し規約(を指示する属性)が記述されています(和訳は筆者)。

規約 内容
@convention(thin) "thin"関数ポインタを示す。Swift呼び出し規約を利用する。「self」およびコンテキストは使用しない
@convention(thick) "thick"関数ポインタを示す。Swift呼び出し規約を利用する。関数が必要とするキャプチャやその他の状態を表すための、参照カウントされたコンテキストオブジェクトを持つ。この属性は@callee_ownedまたは@callee_guaranteedが指定された場合、暗黙的に付加される
@convention(block) Objective-C互換のブロックポインタを示す。関数の値はブロックオブジェクトへの参照として表現される。このブロックオブジェクトは、そのオブジェクト内に自身の呼び出し関数を埋め込んだid互換のObjective-Cオブジェクト。自身の呼び出し関数はCの呼び出し規約を使用する
@convention(c) C言語の関数ポインタを示す。関数の値は、C言語の呼び出し規約を利用する。コンテキストは使用しない
@convention(objc_method) Objective-Cのメソッド実装を示す。関数はC言語の呼び出し規約を利用する。SILレベルでの「self」パラメータ(SILの規約により最終的な形式パラメータにマッピングされる)が、「self」および「_cmd」引数にマッピングされる
@convention(method) Swiftのインスタンスメソッド実装を示す。関数はSwift呼び出し規約を利用する。「self」パラメータを使用する
@convention(witness_method) Swiftのプロトコルメソッド実装を示す。プロトコルのあらゆる実装に対してポリモーフィック性が保証されるように、関数のポリモーフィックな呼び出し規約が生成される

ここで「thin(薄い)」「thick(厚い)」は以下の通りです。

* thin:型の内部のデータだけで済む型。つまり参照・ポインタを持たない型
* thick:参照・ポインタ、もしくは内部に参照・ポインタを持つ型

他のSwiftのドキュメントでは「Trivial Types(自明型)」という表現も使われることがあります。thin≒Trivial Typeと考えて良いのではないかと思っています。

thin関数とは引数がthinのみの関数で、thick関数とはthickな引数が1つ以上含まれている関数です(という理解でいますが、間違っているかも知れません)。
(↑間違っていました)
現時点では、thick関数とはclassなどthickなオブジェクトのメンバ関数、thin関数はそうでない関数、と推測します。
thickな関数はクロージャです。thinは通常の関数です。thick/block/c/objc_method/method/witness_methodに該当しない関数がthinになります。

kabeyakabeya

引数・返値の所有権規約

所有権の種類として、「None」「Owned」「Guaranteed」「Unowned」「Any」があります。
SILレベルの引数には「@owned」「@guaranteed」「@unowned」のマークが付くことがあります。
ドキュメントには@noneというキーワードも出てはいるのですが、それが実際に使われるかどうかまでは分かりませんでした。

kabeyakabeya

マシン語(アセンブラ)レベルの呼び出し規約

サマリ版が「Calling Convention Summary」にあります。

サマリでないものについては以下の通りです。

今回はARM64には触れずにx86だけ見ていくことにします。

x86_64の呼び出し規約

ざっくり言うと、以下のようになります。

  1. 整数型の引数をrdirsirdxrcxr8r9の6個のレジスタに引数順に入れる
  2. 浮動小数点数の引数をxmm0xmm7の8個のレジスタに引数順に入れる
  3. 余った引数はスタックに引数の逆順に積む(後ろの引数が最初にスタックに積まれる。これによりスタックから取り出すとき、後ろの引数が最後に取り出される)
  4. 返値はraxrdxrcxr8レジスタ(浮動小数点数はxmm0xmm8st0st1)を使って返す
kabeyakabeya

確認

ここからは実際にコードを書いてコンパイルして出力を確認します。

確認したい組み合わせ

少なくとも高級言語レベルでの規約で触れた「引数の渡し方」の組み合わせは調べたいと思います。

  1. 参照による引数渡し vs 値による引数渡し
  2. inout vs borrowing vs consuming vs なし
  3. 参照型 vs 値型(ではなくてthick vs thinなのかも)
  4. let vs var
  5. 「イニシャライザ、プロパティセッタ」 vs 「それ以外の関数」

Copyable~Copyableの件は制約だけの話なので、いったん今回の調査(アセンブラの出力まで確認)の対象からは除外します。

また2×4×2×2×2=64通りありますので、何かは省略するかも知れません。
(いま想定しているのは、letvarは一部の組み合わせだけ調べれば充分そう、ということです。それ以外にもあるかも知れません)

kabeyakabeya

最初の調査

最初の調査は、シンプルなものということで以下を調査します。

  • thin
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

thinな引数

thinな引数用のデータ型として、整数値のみを持つstructを定義します。

struct TestData {
    var value1: Int = 0
    var value2: Int = 0
}

まずはvarで定義した変数を普通の関数(イニシャライザでもプロパティセッタでもない関数)に渡します。
受け取る側の関数にはパラメータ修飾子(inout/borrowing/consuming)は付けません。
また、値が使われないとコンパイラがコードを省略してしまう可能性があるので、もらった値はグローバル変数にセットします。

var globalValue: Int = 0

func testFunc(_ testData: TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
	testFunc(testData)
}
kabeyakabeya

SIL

これのSILを順に見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test18mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack $TestData, var, name "testData" // users: %8, %11
005:   %1 = metatype $@thin TestData.Type              // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.init(value1:value2:)
011:   %6 = function_ref @$s5Test18TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // users: %8, %10
013:   store %7 to %0 : $*TestData                     // id: %8
014:   // function_ref testFunc(_:)
015:   %9 = function_ref @$s5Test18testFuncyyAA8TestDataVF : $@convention(thin) (TestData) -> () // user: %10
016:   %10 = apply %9(%7) : $@convention(thin) (TestData) -> ()
017:   dealloc_stack %0 : $*TestData                   // id: %11
018:   %12 = tuple ()                                  // user: %13
019:   return %12 : $()                                // id: %13
020: } // end sil function '$s5Test18mainFuncyyF'

004: TestDataのメモリ領域をスタックに確保し、%0にそのアドレスを入れる
005: 変数%1TestDataの型情報を入れる
006: 変数%2に整数リテラル3を入れる
007: 変数%3Int構造体(3)を入れる
008: 変数%4に整数リテラル4を入れる
009: 変数%5Int構造体(4)を入れる
011: 変数%6TestData.init(value1:value2:)の関数ポインタを入れる
012: TestData.init(value1:value2:)%6の関数ポインタにより呼び出す。引数はInt構造体(34)と、TestDataの型情報。クラスメソッドの呼び出しになっている
012: TestData.init(value1:value2:)により返ってきたTestDataは変数%7に入る
013: %0の指すアドレスに確保されているメモリ領域に、%7に入っている初期化されたTestDataを入れる
015: 変数%9testFunc(_:)の関数ポインタを入れる
016: testFunc(_:)%9の関数ポインタにより呼び出す。引数は%7に入っているTestData
017: 最初に確保したTestDataのメモリ領域を開放する
018: 空のタプルを返す。

SIL上では、testFuncを呼び出すときに明示的に引数をコピーしているというわけではないんですね。

次いでtestFuncです。

001: // testFunc(_:)
002: sil hidden @$s5Test18testFuncyyAA8TestDataVF : $@convention(thin) (TestData) -> () {
003: // %0 "testData"                                  // users: %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s5Test111globalValueSivp : $*Int // user: %4
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = struct_extract %0 : $TestData, #TestData.value2 // user: %5
008:   %4 = begin_access [modify] [dynamic] %1 : $*Int // users: %5, %6
009:   store %3 to %4 : $*Int                          // id: %5
010:   end_access %4 : $*Int                           // id: %6
011:   %7 = tuple ()                                   // user: %8
012:   return %7 : $()                                 // id: %8
013: } // end sil function '$s5Test18testFuncyyAA8TestDataVF'

004: 変数%0に引数testData: TestDataを入れる
005: 変数%1にグローバル変数globalValueのアドレス(ポインタ)を入れる
007: 変数%3TestData.value2の値を入れる
008: グローバル変数globalValueへのアクセスを開始する。[dynamic]は、このアクセスが排他的に行われているか実行時にチェックすることを示す
008: begin_accessで得られたポインタを変数%4にセットする
009: 変数%3の値を変数%4のポインタの指すアドレスに入れる
010: begin_accessで得られたポインタへのアクセスを終了する
012: 空のタプルを返す。

testData:を受け取ったときにさらに何かするということもなく、受け取ったまま使っています。

kabeyakabeya

アセンブラ

続いてアセンブラを順に見ていきます。まずmainFuncから。

001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 16], xmm0
007:         mov     edi, 3
008:         mov     esi, 4
009:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
010:         mov     rdi, rax
011:         mov     rsi, rdx
012:         mov     qword ptr [rbp - 16], rdi
013:         mov     qword ptr [rbp - 8], rsi
014:         call    (output.testFunc(output.TestData) -> ())
015:         add     rsp, 16
016:         pop     rbp
017:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: スタックポインタから16バイト引く
005: xmm0レジスタの値同士でxorpsする。意味合い的にはxmm0のゼロクリア
006: ベースポインタから16バイト引いたアドレスにxmm0の値(=0)をセット
007: ediレジスタに3をセット
008: esiレジスタに4をセット
009: TestData.init(value1:value2:)を呼び出し
010: rdiレジスタにraxレジスタの値をセット
011: rsiレジスタにrdxレジスタの値をセット
012: ベースポインタから16バイト引いたアドレスにrdiレジスタの値をセット
013: ベースポインタから8バイト引いたアドレスにrsiレジスタの値をセット
014: testFunc(testData:)を呼び出し
015: スタックポインタに16バイト足す
016: スタックに待避してあったベースポインタを復元
017: リターン

004の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-16 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-16〜rbp-9 testData.value1分
rsp+8〜rsp+15 rbp-8〜rbp-1 testData.value2分
rsp+16 rbp ベースポインタ
rsp+16〜rsp+23 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

005〜006では、value1の64ビットとvalue2の64ビットの計128ビット分に対し、浮動小数点レジスタを使って1回で0をセットしています。
007〜008のediレジスタというのはrdiレジスタ(64bit)の下位32ビットのこと、esiは同様にrsiの下位32ビットです。
009は第1引数=3→rdi(edi)、第2引数=4→rsi(esi)にセットしてTestData.init(value1:value2:)を呼んでいます。
返値はTestData構造体ですが64ビットのデータ2つなので、rax、rdxの2つのレジスタで返ってきます。
010〜011で、rax(=value1)、rdxレジスタ(=value2)に返ってきた構造体のフィールドをrdi,rsiレジスタにいったん入れます。これは014で引き続きtestFunc(testData:)を呼び出すための準備でもあります。
さらに012〜013で、レジスタからメモリに値をコピーします。
014で、第1引数=value1→rdi、第2引数=value2→rsiとなっている状態でtestFunc(testData:)を呼び出します。構造体が引数の場合は、個々の要素に展開して並べて、順に引数として渡します。

SILではTestData.init(value1:value2:)に型情報を渡していましたが、アセンブラだと渡していません。

続いてtestFuncです。

001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 64
005:         mov     qword ptr [rbp - 56], rsi
006:         xorps   xmm0, xmm0
007:         movaps  xmmword ptr [rbp - 16], xmm0
008:         mov     qword ptr [rbp - 16], rdi
008:         mov     qword ptr [rbp - 8], rsi
010:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
011:         xor     eax, eax
012:         mov     ecx, eax
013:         lea     rsi, [rbp - 40]
014:         mov     qword ptr [rbp - 48], rsi
015:         mov     edx, 33
016:         call    swift_beginAccess@PLT
017:         mov     rsi, qword ptr [rbp - 56]
018:         mov     rdi, qword ptr [rbp - 48]
019:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rsi
020:         call    swift_endAccess@PLT
021:         add     rsp, 64
022:         pop     rbp
023:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: スタックポインタから64バイト引く
005: ベースポインタから56バイト引いたアドレスにrsiの値をセット
006: xmm0レジスタの値同士でxorpsする。意味合い的にはxmm0のゼロクリア
007: ベースポインタから16バイト引いたアドレスにxmm0の値(=0)をセット
008: ベースポインタから16バイト引いたアドレスにrdiレジスタの値をセット
009: ベースポインタから8バイト引いたアドレスにrsiレジスタの値をセット
010: rdiレジスタにglobalValue変数のアドレス(ポインタ)をセット
011: eaxレジスタをゼロクリア
012: ecxレジスタにeaxレジスタの値(=0)をセット
013: rsiレジスタに、ベースポインタから40バイト引いたアドレス(ポインタ)をセット
014: ベースポインタから48バイト引いたアドレスにrsiレジスタの値をセット
015: edxレジスタに33をセット
016: swift_beginAccess関数を呼ぶ
017: ベースポインタから56バイト引いたアドレスより値を取得してrsiレジスタにセット
018: ベースポインタから48バイト引いたアドレスより値を取得してrdiレジスタにセット
019: globalValue変数のアドレスにrsiレジスタの値をセット
020: swift_endAccess関数を呼ぶ
021: スタックポインタに64バイト足す
022: スタックに待避してあったベースポインタを復元
023: リターン

004の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-64 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-64〜rbp-57
rsp+8〜rsp+15 rbp-56〜rbp-49 globalValue代入用一時変数
rsp+16〜rsp+23 rbp-48〜rbp-41 swift_endAccess用のValueBuffer
rsp+24〜rsp+31 rbp-40〜rbp-33 swift_beginAccess用のValueBuffer
rsp+32〜rsp+39 rbp-32〜rbp-25
rsp+40〜rsp+47 rbp-24〜rbp-17
rsp+48〜rsp+55 rbp-16〜rbp-9 ローカルTestData.value1分
rsp+56〜rsp+63 rbp-8〜rbp-1 ローカルTestData.value2分
rsp+64 rbp ベースポインタ
rsp+64〜rsp+71 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

関数呼び出し時点で、第1引数=value1→rdi、第2引数=value2→rsiに入っています。SILでの引数は構造体ですが、要素が展開されて順に渡ってきています。
005では、第2引数(rsi)=value2の値を一時変数に入れています。
006〜009で、引数で受け取った値を、TestData型ローカル変数にセットしています。
010〜015でswift_beginAccess関数を呼ぶための引数を準備しています。

swift_beginAccessおよびswift_endAccessの関数プロトタイプは以下です。

include/swift/Runtime/Exclusivity.h
void swift::swift_beginAccess(void *pointer, ValueBuffer *buffer,ExclusivityFlags flags, void *pc);
void swift::swift_endAccess(ValueBuffer *buffer);

第1引数のpointerはグローバル変数などアクセス制御対象変数のアドレスです。rdiを使って渡します。
第2引数のbufferswift_endAccessと対になる作業用のバッファです。rsiを使って渡します。
第3引数のflagsはアクセスの排他性を示す定数です。rdxを使って渡します。
第4引数のpcはプログラムカウンタです。次の実行命令のアドレスを指します。これをNULL(=0)で渡すと、このswift_beginAccessの呼び出しの直後になります(のではないかと想像します。調べ切れていません)。rcxを使って渡します。

第3引数に指定されているExclusivityFlagsの定義は以下です。

include/swift/ABI/MetadataValues.h
enum class ExclusivityFlags : uintptr_t {
  Read             = 0x0,
  Modify           = 0x1,
  // ActionMask can grow without breaking the ABI because the runtime controls
  // how these flags are encoded in the "value buffer". However, any additional
  // actions must be compatible with the original behavior for the old, smaller
  // ActionMask (older runtimes will continue to treat them as either a simple
  // Read or Modify).
  ActionMask       = 0x1,

  // The runtime should track this access to check against subsequent accesses.
  Tracking         = 0x20
};

コード中で指定されている33は0x21で、Tracking+Modifyを意味します。
SILでの[modify] [dynamic]に相当します。

swift_beginAccess呼び出しのための引数準備は以下のようになっています。

  • 第1引数:010でglobalValue変数のアドレスがrdiに入る
  • 第2引数:013でベースポインタから40引いたアドレスがrsiに入る
  • 第3引数:015で33(=0x21)がedx(=rdx)に入る
  • 第4引数:012で0がecx(=rcx)に入る

さらに、014では第2引数の値が、ベースポインタから48引いたアドレスにコピーされます。

これでswift_beginAccessを呼ぶ準備ができ、016で実際にswift_beginAccessを呼びます。
@PLTのPLTはProcedure Linkage Tableの略で、遅延ロードされる共有ライブラリの関数を呼ぶための仕組みです。@PLTは関数アドレス計算方法を示すもので、他の計算方法の指定としては@PLTOFF@GOTなどがあります。

続いて017で、引数のtestData.value2由来の値を一時変数から取得してrsiにセットし、019でrsiの値をglobalValue変数のアドレスにセットします。
そして、018でswift_endAccessの引数(ValueBuffer)を準備します。これは014でメモリにコピーされていたものです。
最後に020でswift_endAccessを呼び出します。

kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thin
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。今回は64ビット値2つ分なのでレジスタだけで収まっているけども、もっと要素が多い場合はスタックにもコピーされる
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする(とは言え、たまたまかも。もう少し調査が必要)

調査の部分の長さのわりに、まとめがたいしたことなくて済みません。

kabeyakabeya

2個目の調査

2個目の調査は、最初の調査とほぼ同じもので以下を調査します。

  • thin
  • var → let
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし
var globalValue: Int = 0

func testFunc(_ testData: TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    let testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}
kabeyakabeya

SIL

これのSILを順に見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test28mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = metatype $@thin TestData.Type              // user: %6
005:   %1 = integer_literal $Builtin.Int64, 3          // user: %2
006:   %2 = struct $Int (%1 : $Builtin.Int64)          // user: %6
007:   %3 = integer_literal $Builtin.Int64, 4          // user: %4
008:   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %6
009:   // function_ref TestData.init(value1:value2:)
010:   %5 = function_ref @$s5Test28TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %6
011:   %6 = apply %5(%2, %4, %0) : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // users: %9, %7
012:   debug_value %6 : $TestData, let, name "testData" // id: %7
013:   // function_ref testFunc(_:)
014:   %8 = function_ref @$s5Test28testFuncyyAA8TestDataVF : $@convention(thin) (TestData) -> () // user: %9
015:   %9 = apply %8(%6) : $@convention(thin) (TestData) -> ()
016:   %10 = tuple ()                                  // user: %11
017:   return %10 : $()                                // id: %11
018: } // end sil function '$s5Test28mainFuncyyF'

変わったところだけ説明します。

  • alloc_stack/dealloc_stackおよびstoreがなくなっている。varでなくletになったことで、メモリ確保、メモリへの明示的なロードがなくなった

あとは同じです。

ただ、これを見るとvarのときはローカル変数と渡す値とを分けている(コピーしている)が、letではコピーがないというようにも考えられます。この辺はもう少し調査したほうが良さそうです。

次いでtestFuncです。

001: // testFunc(_:)
002: sil hidden @$s5Test28testFuncyyAA8TestDataVF : $@convention(thin) (TestData) -> () {
003: // %0 "testData"                                  // users: %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s5Test211globalValueSivp : $*Int // user: %4
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = struct_extract %0 : $TestData, #TestData.value2 // user: %5
008:   %4 = begin_access [modify] [dynamic] %1 : $*Int // users: %5, %6
009:   store %3 to %4 : $*Int                          // id: %5
010:   end_access %4 : $*Int                           // id: %6
011:   %7 = tuple ()                                   // user: %8
012:   return %7 : $()                                 // id: %8
013: } // end sil function '$s5Test28testFuncyyAA8TestDataVF'

こちらは最初の例と完全に一致しています。

アセンブラ

続いてアセンブラを順に見ていきます。まずmainFuncから。

001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 16], xmm0
007:         mov     edi, 3
008:         mov     esi, 4
009:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
010:         mov     rdi, rax
011:         mov     rsi, rdx
012:         mov     qword ptr [rbp - 16], rdi
013:         mov     qword ptr [rbp - 8], rsi
014:         call    (output.testFunc(output.TestData) -> ())
015:         add     rsp, 16
016:         pop     rbp
017:         ret

アセンブラはSILと違い、最初の例と完全に一致しています。

続いてtestFuncです。

001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 64
005:         mov     qword ptr [rbp - 56], rsi
006:         xorps   xmm0, xmm0
007:         movaps  xmmword ptr [rbp - 16], xmm0
008:         mov     qword ptr [rbp - 16], rdi
009:         mov     qword ptr [rbp - 8], rsi
010:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
011:         xor     eax, eax
012:         mov     ecx, eax
013:         lea     rsi, [rbp - 40]
014:         mov     qword ptr [rbp - 48], rsi
015:         mov     edx, 33
016:         call    swift_beginAccess@PLT
017:         mov     rsi, qword ptr [rbp - 56]
018:         mov     rdi, qword ptr [rbp - 48]
019:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rsi
020:         call    swift_endAccess@PLT
021:         add     rsp, 64
022:         pop     rbp
023:         ret

こちらも最初の例と完全に一致しています。

まとめ

今回は以下のケースで調査しました。

  • thin
  • var → let
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。今回は64ビット値2つ分なのでレジスタだけで収まっているけども、もっと要素が多い場合はスタックにもコピーされる
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする(とは言え、たまたまかも。もう少し調査が必要)
  • SILレベルではvarのときと差があるがアセンブラレベルでは一致してしまった。ただし例が簡単すぎる可能性もある。もう少し調査が必要

結果からさかのぼって、例の作り方を再考する必要がありますね。

kabeyakabeya

3個目の調査

3個目の調査は、最初の調査とほぼ同じもので以下を調査します。

  • thin
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

3個目は、イニシャライザでの動作を見ます。

struct Foo {
    var propValue: Int = 0
    
    init(_ testData: TestData) {
        self.propValue = testData.value2
    }
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    var foo = Foo(testData)
    foo.propValue = 5
}
kabeyakabeya

SIL

これのSILを順に見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test38mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack $TestData, var, name "testData" // users: %8, %21
005:   %1 = metatype $@thin TestData.Type              // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.init(value1:value2:)
011:   %6 = function_ref @$s5Test38TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // users: %8, %12
013:   store %7 to %0 : $*TestData                     // id: %8
014:   %9 = alloc_stack $Foo, var, name "foo"          // users: %13, %20, %16
015:   %10 = metatype $@thin Foo.Type                  // user: %12
016:   // function_ref Foo.init(_:)
017:   %11 = function_ref @$s5Test33FooVyAcA8TestDataVcfC : $@convention(method) (TestData, @thin Foo.Type) -> Foo // user: %12
018:   %12 = apply %11(%7, %10) : $@convention(method) (TestData, @thin Foo.Type) -> Foo // user: %13
019:   store %12 to %9 : $*Foo                         // id: %13
020:   %14 = integer_literal $Builtin.Int64, 5         // user: %15
021:   %15 = struct $Int (%14 : $Builtin.Int64)        // user: %18
022:   %16 = begin_access [modify] [static] %9 : $*Foo // users: %19, %17
023:   %17 = struct_element_addr %16 : $*Foo, #Foo.propValue // user: %18
024:   store %15 to %17 : $*Int                        // id: %18
025:   end_access %16 : $*Foo                          // id: %19
026:   dealloc_stack %9 : $*Foo                        // id: %20
027:   dealloc_stack %0 : $*TestData                   // id: %21
028:   %22 = tuple ()                                  // user: %23
029:   return %22 : $()                                // id: %23
030: } // end sil function '$s5Test38mainFuncyyF'

前半は最初の例と同じなので014から見ていきます。

014: Fooのメモリ領域をスタックに確保し、%9にそのアドレスを入れる
015: 変数%10Fooの型情報を入れる
017: 変数%11Foo.init(_:)の関数ポインタを入れる
018: Foo.init(_:)%11の関数ポインタにより呼び出す。引数は%7に入っている初期化されたTestDataと、Fooの型情報。クラスメソッドの呼び出しになっている
018: Foo.init(_:)により返ってきたFooは変数%12に入る
019: %9の指すアドレスに確保されているメモリ領域に、%12に入っている初期化されたFooを入れる
020: 変数%14に整数リテラル5を入れる
021: 変数%15Int構造体(5)を入れる
022: %9の指すインスタンスへのアクセスを開始する。[static]は、このアクセスが排他的に行われているかコンパイル時にのみチェックして実行時にはチェックしないことを示す
023: %17FooのインスタンスのpropValueプロパティのアドレスを入れる
024: %17のアドレスにInt構造体(5)を入れる
025: Fooのインスタンスへのアクセスを終了する
026: 確保したFooのメモリ領域を開放する
027: 確保したTestのメモリ領域を開放する
029: 空のタプルを返す。

イニシャライザの呼び出し(014〜019)も、普通の関数(イニシャライザ、プロパティセッタ以外の関数)と違わないように見えます。

次いで、イニシャライザです。

001: // Foo.init(_:)
002: sil hidden @$s5Test33FooVyAcA8TestDataVcfC : $@convention(method) (TestData, @thin Foo.Type) -> Foo {
003: // %0 "testData"                                  // users: %8, %3
004: // %1 "$metatype"
005: bb0(%0 : $TestData, %1 : $@thin Foo.Type):
006:   %2 = alloc_stack $Foo, var, name "self", implicit // users: %7, %9, %14
007:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %3
008:   %4 = integer_literal $Builtin.Int64, 0          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
010:   %6 = struct $Foo (%5 : $Int)                    // user: %7
011:   store %6 to %2 : $*Foo                          // id: %7
012:   %8 = struct_extract %0 : $TestData, #TestData.value2 // users: %11, %13
013:   %9 = begin_access [modify] [static] %2 : $*Foo  // users: %12, %10
014:   %10 = struct_element_addr %9 : $*Foo, #Foo.propValue // user: %11
015:   store %8 to %10 : $*Int                         // id: %11
016:   end_access %9 : $*Foo                           // id: %12
017:   %13 = struct $Foo (%8 : $Int)                   // user: %15
018:   dealloc_stack %2 : $*Foo                        // id: %14
019:   return %13 : $Foo                               // id: %15
020: } // end sil function '$s5Test33FooVyAcA8TestDataVcfC'

005: 変数%0に引数testData: TestDataを入れる
005: 変数%1Fooの型情報を入れる
006: Fooのメモリ領域をスタックに確保し、%2にそのアドレスを入れる
008: 変数%4に整数リテラル0を入れる
009: 変数%5Int構造体(0)を入れる
010: 変数%6Foo構造体(0)を入れる
011: %2の指すアドレスに確保されているメモリ領域に、%6に入っている初期化されたFooを入れる
012: 変数%8%0testDataからvalue2を取得して入れる
013: %2の指すインスタンスへのアクセスを開始する。[static]は、このアクセスが排他的に行われているかコンパイル時にのみチェックして実行時にはチェックしないことを示す
014: %10FooのインスタンスのpropValueプロパティのアドレスを入れる
015: %10のアドレスに%8の値(=testData.value2)を入れる
016: Fooのインスタンスへのアクセスを終了する
017: 変数%13Foo構造体(変数%8で初期化したもの)を入れる
018: 確保したFooのメモリ領域を開放する
019: 変数%13を返す

なんでしょうか、奇妙です。
006でalloc_stackした変数は、019で返すのに使っていません。スタックに確保された領域なのでそれ自体を返さないのは分かりますが、017で%2が使われていないのが不思議です。

これはアセンブラがどうなっているのか興味あります。

kabeyakabeya

アセンブラ

アセンブラを順に見ていきます。まずmainFuncから。

001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 32
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 16], xmm0
007:         mov     qword ptr [rbp - 24], 0
008:         mov     edi, 3
009:         mov     esi, 4
010:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
011:         mov     rdi, rax
012:         mov     rsi, rdx
013:         mov     qword ptr [rbp - 16], rdi
014:         mov     qword ptr [rbp - 8], rsi
015:         call    (output.Foo.init(output.TestData) -> output.Foo)
016:         mov     qword ptr [rbp - 24], rax
017:         mov     qword ptr [rbp - 24], 5
018:         add     rsp, 32
019:         pop     rbp
020:         ret

Fooの初期化などがありますが、最初の例とほとんど同じです。
初期化されたFooの受け取りかたとしては、propValueの値をraxレジスタで受け取るだけ(016)、といった感じになっています。

004の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-32 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-32〜rbp-23
rsp+8〜rsp+15 rbp-24〜rbp-15 foo.propValue分
rsp+16〜rsp+23 rbp-16〜rbp-7 testData.value1分
rsp+24〜rsp+31 rbp-8〜rbp-1 testData.value2分
rsp+32 rbp ベースポインタ
rsp+32〜rsp+39 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

続いてFoo.init(_:)です。

001: output.Foo.init(output.TestData) -> output.Foo:
002:         push    rbp
003:         mov     rbp, rsp
004:         mov     rax, rsi
005:         mov     qword ptr [rbp - 8], 0
006:         xorps   xmm0, xmm0
007:         movaps  xmmword ptr [rbp - 32], xmm0
008:         mov     qword ptr [rbp - 32], rdi
009:         mov     qword ptr [rbp - 24], rax
010:         mov     qword ptr [rbp - 8], 0
011:         mov     qword ptr [rbp - 8], rax
012:         pop     rbp
013:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: raxレジスタにrsiレジスタの値(第2引数=testData.value2)をセット
005: ベースポインタから8バイト引いたアドレスに0をセット
006: xmm0レジスタの値同士でxorpsする。意味合い的にはxmm0のゼロクリア
007: ベースポインタから32バイト引いたアドレスにxmm0の値(=0)をセット
008: ベースポインタから32バイト引いたアドレスにrdiレジスタの値(第1引数=testData.value1)をセット
009: ベースポインタから24バイト引いたアドレスにraxレジスタの値(=rsiレジスタの値=第2引数=testData.value2)をセット
010: ベースポインタから8バイト引いたアドレスに0をセット
011: ベースポインタから8バイト引いたアドレスにraxレジスタ(=rsiレジスタの値=第2引数=testData.value2)の値をセット
012: スタックに待避してあったベースポインタを復元
013: リターン

003の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp−32〜rsp-23 rbp-32〜rbp-23 testData.value1分 アドレスの小さいほう
rsp-24〜rsp-15 rbp-24〜rbp-15 testData.value2分
rsp-16〜rsp-7 rbp-16〜rbp-7
rsp-8〜rsp-1 rbp-8〜rbp-1 self(=Foo).propValue分
rsp rbp スタックポインタ&ベースポインタ
rsp〜rsp+7 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

初めて気付きましたが、関数内で他の関数を呼び出さない場合スタックポインタの値は変えないんですね[1]
(アセンブラに触れるのは25年ぶりぐらいなので、これが標準なのかどうなのかももう分かりません)

それはともかく、SILのまわりくどさがアセンブラにも現れています。
005〜011でローカル変数を色々設定するのですが、結局それらは使わず、004でraxレジスタにrsiレジスタの値を入れたものが最終的に返される、という動きになっています。
これは何かコンパイラのオプションを設定すると、005〜011の無駄な処理が切り落とされるのでしょうか。

脚注
  1. あとでCalling Convention Summaryを見たら「Frameless leaf functions, however, will often not set up the frame pointer, rbp, in which case they may refer to arguments relative to rsp instead.」と書いてありました。 ↩︎

kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thin
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。今回は64ビット値2つ分なのでレジスタだけで収まっているけども、もっと要素が多い場合はスタックにもコピーされる
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする(とは言え、たまたまかも。もう少し調査が必要)
  • イニシャライザとそれ以外とで、特に大きな違いはなさそう
kabeyakabeya

4個目の調査

4個目の調査は、structの調査に飽きてきたので以下を調査します。

  • thick
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

thickな引数

thickな引数用のデータ型として、ここまでのTestData構造体をclassに変えてみます。
あとは最初の調査と同じにします。

class TestData {
    var value1: Int = 0
    var value2: Int = 0
    
    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

var globalValue: Int = 0

func testFunc(_ testData: TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}
kabeyakabeya

SIL

SILを見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test48mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %9, %14, %13
005:   %1 = metatype $@thick TestData.Type             // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.__allocating_init(value1:value2:)
011:   %6 = function_ref @$s5Test48TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // users: %12, %9, %11, %8
013:   strong_retain %7 : $TestData                    // id: %8
014:   store %7 to %0 : $*TestData                     // id: %9
015:   // function_ref testFunc(_:)
016:   %10 = function_ref @$s5Test48testFuncyyAA8TestDataCF : $@convention(thin) (@guaranteed TestData) -> () // user: %11
017:   %11 = apply %10(%7) : $@convention(thin) (@guaranteed TestData) -> ()
018:   strong_release %7 : $TestData                   // id: %12
019:   destroy_addr %0 : $*TestData                    // id: %13
020:   dealloc_stack %0 : $*TestData                   // id: %14
021:   %15 = tuple ()                                  // user: %16
022:   return %15 : $()                                // id: %16
023: } // end sil function '$s5Test48mainFuncyyF'

ちょっとここでごめんなさいがあります。

先に「thin関数とは引数がthinのみの関数で、thick関数とはthickな引数が1つ以上含まれている関数です(という理解でいますが、間違っているかも知れません)」と書いていましたが、間違っていました。

017は、引数がclass TestDataなのでthickな引数ですが、関数は@convention(thin)です。
改めてルールを振り返って@convention(thin)@convention(thick)の違いを見ると、selfが要るかどうかということのようです。

structは継承できませんので、メンバ関数の呼び出しにとってはselfはあってないようなものです。selfが実際何なのかによってメモリ配置が変わるということもありませんし、関数がオーバーライドされて別の関数を呼び出すということもありません。

ということで、現時点では「thick関数とはclassなどthickなオブジェクトのメンバ関数、thin関数はそうでない関数」と推測します。
→でもそれだと@convention(method)になる気もしますね。なんでしょう。

kabeyakabeya

いったんthin関数/thick関数の話は置いておいてSILに戻ります。

全体の大まかな流れは最初の例と同じですが、何点か違いがあります。

  1. 関数ポインタの型の返値に@ownedがついている(011)
  2. 関数ポインタの型の引数に@guaranteedがついている(016)
  3. strong_retainstrong_releaseの呼び出しがある(013、018)
  4. destroy_addrの呼び出しがある(019)

1については、TestDataクラスのイニシャライザで返ってきたオブジェクトは呼び出し元が所有するんですよ、というような意味合いでしょう。
実際、このイニシャライザから返ってきた012の段階で参照カウンタは1になっています。
その後、013でstrong_retainしますが、これによって参照カウンタは2になります。

2については、testFunc(_:)を呼び出している間はずっと引数のTestDataの存在が保証されるよ、という意味合いです。
呼び出しから返ってきたら、strong_releaseで参照カウンタを下げます。これにより参照カウンタは1になります。

ややこしいのは4のdestory_addrです。こいつは「引数のアドレスの指すものが自明型(ポインタを含まない型。整数など)なら何もせず、非自明型ならそのアドレスからオブジェクトをロードして、strong_releaseをします」みたいなことがドキュメントには書いてあります。
ですが、~Copyablestructの場合、こいつはdeinitを呼び出します。structstrong_releaseはできないので呼ばれないのは当然ですが、deinitするんですね。ややこしい。

ともかく018で参照カウンタが2→1になり、019で1→0になります。これによってデイニシャライザが呼ばれ、ついでメモリが解放されます。

ここでTestDataのイニシャライザのSILを見てみます。

001: // TestData.__allocating_init(value1:value2:)
002: sil hidden [exact_self_class] @$s5Test48TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData {
003: // %0 "value1"                                    // user: %5
004: // %1 "value2"                                    // user: %5
005: // %2 "$metatype"
006: bb0(%0 : $Int, %1 : $Int, %2 : $@thick TestData.Type):
007:   %3 = alloc_ref $TestData                        // user: %5
008:   // function_ref TestData.init(value1:value2:)
009:   %4 = function_ref @$s5Test48TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %5
010:   %5 = apply %4(%0, %1, %3) : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %6
011:   return %5 : $TestData                           // id: %6
012: } // end sil function '$s5Test48TestDataC6value16value2ACSi_SitcfC'
013: 
014: // TestData.init(value1:value2:)
015: sil hidden @$s5Test48TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData {
016: // %0 "value1"                                    // users: %16, %3
017: // %1 "value2"                                    // users: %20, %4
018: // %2 "self"                                      // users: %18, %14, %10, %6, %22, %5
019: bb0(%0 : $Int, %1 : $Int, %2 : $TestData):
020:   debug_value %0 : $Int, let, name "value1", argno 1 // id: %3
021:   debug_value %1 : $Int, let, name "value2", argno 2 // id: %4
022:   debug_value %2 : $TestData, let, name "self", argno 3, implicit // id: %5
023:   %6 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %9
024:   %7 = integer_literal $Builtin.Int64, 0          // user: %8
025:   %8 = struct $Int (%7 : $Builtin.Int64)          // user: %9
026:   store %8 to %6 : $*Int                          // id: %9
027:   %10 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %13
028:   %11 = integer_literal $Builtin.Int64, 0         // user: %12
029:   %12 = struct $Int (%11 : $Builtin.Int64)        // user: %13
030:   store %12 to %10 : $*Int                        // id: %13
031:   %14 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %15
032:   %15 = begin_access [modify] [dynamic] %14 : $*Int // users: %16, %17
033:   store %0 to %15 : $*Int                         // id: %16
034:   end_access %15 : $*Int                          // id: %17
035:   %18 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %19
036:   %19 = begin_access [modify] [dynamic] %18 : $*Int // users: %20, %21
037:   store %1 to %19 : $*Int                         // id: %20
038:   end_access %19 : $*Int                          // id: %21
039:   return %2 : $TestData                           // id: %22
040: } // end sil function '$s5Test48TestDataC6value16value2ACSi_Sitcfc'

__allocating_init(value1:value2:)init(value1:value2:)という2つの関数から成り立っています。
007のalloc_refは、いままでのalloc_stackと違い、スタックではなくヒープに参照型オブジェクトのメモリ領域を割り当てたうえ、参照カウンタを1にしてポインタを返す関数です。
そして009でinit(value1:value2:)を呼び出します。メンバ変数へのアクセスはすべてbegin_access/end_accessで、排他性が動的チェックされていますが、それ以外で特に参照カウンタを上下させたりはしていません。

最後、testFuncを見てみます。

001: // testFunc(_:)
002: sil hidden @$s5Test48testFuncyyAA8TestDataCF : $@convention(thin) (@guaranteed TestData) -> () {
003: // %0 "testData"                                  // users: %4, %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s5Test411globalValueSivp : $*Int // user: %5
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = class_method %0 : $TestData, #TestData.value2!getter : (TestData) -> () -> Int, $@convention(method) (@guaranteed TestData) -> Int // user: %4
008:   %4 = apply %3(%0) : $@convention(method) (@guaranteed TestData) -> Int // user: %6
009:   %5 = begin_access [modify] [dynamic] %1 : $*Int // users: %6, %7
010:   store %4 to %5 : $*Int                          // id: %6
011:   end_access %5 : $*Int                           // id: %7
012:   %8 = tuple ()                                   // user: %9
013:   return %8 : $()                                 // id: %9
014: } // end sil function '$s5Test48testFuncyyAA8TestDataCF'

いままでの例と比べても特に目立ったことはしていません。
1つ言えるのは、やはりここでも参照カウンタの操作はしていない、ということです。

kabeyakabeya

アセンブラ

アセンブラを順に見ていきます。まずmainFuncから。

001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 16], 0
007:         xor     eax, eax
008:         mov     edi, eax
009:         call    (type metadata accessor for output.TestData)
010:         mov     r13, rax
011:         mov     edi, 3
012:         mov     esi, 4
013:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
014:         mov     rdi, rax
015:         mov     qword ptr [rbp - 24], rdi
016:         call    swift_retain@PLT
017:         mov     rdi, qword ptr [rbp - 24]
018:         mov     qword ptr [rbp - 16], rdi
019:         call    (output.testFunc(output.TestData) -> ())
020:         mov     rdi, qword ptr [rbp - 24]
021:         call    swift_release@PLT
022:         mov     rdi, qword ptr [rbp - 16]
023:         call    swift_release@PLT
024:         add     rsp, 24
025:         pop     r13
026:         pop     rbp
027:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: r13レジスタの値をスタックに待避。r13にはselfが入っている
005: スタックポインタから24バイト引く
006: ベースポインタから16バイト引いたアドレスに0をセット
007: eaxレジスタの値同士でxorする。意味合い的にはeaxのゼロクリア
008: ediレジスタにeaxレジスタの値をセット
009: TestDataのメタデータ取得関数を呼ぶ
010: raxに返ってきたTestDataのメタデータをr13(=self)にセット
011: ediに3をセット
012: esiに4をセット
013: TestData.init(value1:value2:)を呼ぶ
014: rdiレジスタにraxレジスタの値をセット
015: ベースポインタから24バイト引いたアドレスにrdiレジスタの値をセット
016: swift_retainを呼ぶ
017: ベースポインタから24バイト引いたアドレスからrdiレジスタの値にセット
018: ベースポインタから16バイト引いたアドレスにrdiレジスタの値をセット
019: testFunc(_:)を呼ぶ
020: ベースポインタから24バイト引いたアドレスからrdiレジスタの値にセット
021: swift_releaseを呼ぶ
022: ベースポインタから16バイト引いたアドレスからrdiレジスタの値にセット
023: swift_releaseを呼ぶ
024: スタックポインタに16バイト足す
025: スタックに待避してあったr13を復元
026: スタックに待避してあったベースポインタを復元
027: リターン

005の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-32 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-32〜rbp-9
rsp+8〜rsp+15 rbp-24〜rbp-9 testDataのポインタ
rsp+16〜rsp+23 rbp-16〜rbp-9 testDataのポインタ
rsp+24〜rsp+31 rbp-8〜rbp-1 元のself
rsp+32 rbp ベースポインタ
rsp+32〜rsp+39 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

testDataのポインタのためのローカル変数が2つあります。TestData.init(value1:value2:)の返値を受ける用の変数と、testFunc(_:)の呼び出し用の変数という2つではないかと推測します。
SILではalloc_stackが1回しかありませんでしたので、ここに2つできるのが意外です。

何にしても、イニシャライザから返ってきた時点で参照カウントが1になっているのに、さらにretainをしているのは確かですね。このために参照カウントは2になります。
この結果、releaseも1回増えて2回あります。

kabeyakabeya

次はTestData.__allocating_init(value1:value2:)TestData.init(value1:value2:)を見てみます。
まずTestData.__allocating_init(value1:value2:)です。

001: output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 32], r13
007:         mov     qword ptr [rbp - 16], rsi
008:         mov     rax, rdi
009:         mov     rdi, qword ptr [rbp - 32]
010:         mov     qword ptr [rbp - 24], rax
011:         mov     esi, 32
012:         mov     edx, 7
013:         call    swift_allocObject@PLT
014:         mov     rdi, qword ptr [rbp - 24]
015:         mov     rsi, qword ptr [rbp - 16]
016:         mov     r13, rax
017:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
018:         add     rsp, 24
019:         pop     r13
020:         pop     rbp
021:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: r13レジスタの値をスタックに待避。r13にはselfが入っている
005: スタックポインタから24バイト引く
006: ベースポインタから32バイト引いたアドレスにr13レジスタの値(=self)をセット
007: ベースポインタから16バイト引いたアドレスにrsiレジスタの値(=第2引数=value2)をセット
008: raxレジスタにrdiレジスタの値(=第1引数=value1)をセット
009: rdiレジスタにベースポインタから32バイト引いたアドレスの値(=self)をセット
010: ベースポインタから24バイト引いたアドレスにraxレジスタの値(=第1引数=value1)をセット
011: esiレジスタに32をセット
012: edxレジスタに7をセット
013: swift_allocObjectを呼ぶ
014: rdiレジスタにベースポインタから24バイト引いたアドレスの値(=第1引数=value1)をセット
015: rsiレジスタにベースポインタから16バイト引いたアドレスの値(=第2引数=value2)をセット
016: r13レジスタ(=self)にraxレジスタの値(=swift_allocObjectの返値)をセット
017: TestData.init(value1:value2:)を呼ぶ
018: スタックポインタに24バイト足す
019: スタックに待避してあったr13を復元
020: スタックに待避してあったベースポインタを復元
021: リターン

005の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-32 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-32〜rbp-9 self
rsp+8〜rsp+15 rbp-24〜rbp-9 value1
rsp+16〜rsp+23 rbp-16〜rbp-9 value2
rsp+24〜rsp+31 rbp-8〜rbp-1 元のself
rsp+32 rbp ベースポインタ
rsp+32〜rsp+39 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

関数の返値は、TestData.init(value1:value2:)の返値そのままのものがraxレジスタで返ります。
swift_allocObjectはヒープにオブジェクトを生成し、参照カウンタを1で初期化して返します。

kabeyakabeya

ついでTestData.init(value1:value2:)です。

001: output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 144
005:         mov     qword ptr [rbp - 80], r13
006:         mov     qword ptr [rbp - 96], rsi
007:         mov     qword ptr [rbp - 136], rdi
008:         mov     qword ptr [rbp - 8], 0
009:         mov     qword ptr [rbp - 16], 0
010:         mov     qword ptr [rbp - 24], 0
011:         mov     qword ptr [rbp - 8], rdi
012:         mov     qword ptr [rbp - 16], rsi
013:         mov     qword ptr [rbp - 24], r13
014:         mov     rdi, r13
015:         add     rdi, 16
016:         mov     qword ptr [r13 + 16], 0
017:         mov     rax, r13
018:         add     rax, 24
019:         mov     qword ptr [rbp - 120], rax
020:         mov     qword ptr [r13 + 24], 0
021:         xor     eax, eax
022:         mov     ecx, eax
023:         mov     qword ptr [rbp - 104], rcx
024:         lea     rsi, [rbp - 48]
025:         mov     qword ptr [rbp - 128], rsi
026:         mov     edx, 33
027:         mov     qword ptr [rbp - 112], rdx
028:         call    swift_beginAccess@PLT
029:         mov     rax, qword ptr [rbp - 136]
030:         mov     rdi, qword ptr [rbp - 128]
031:         mov     qword ptr [r13 + 16], rax
032:         call    swift_endAccess@PLT
033:         mov     rdi, qword ptr [rbp - 120]
034:         mov     rdx, qword ptr [rbp - 112]
035:         mov     rcx, qword ptr [rbp - 104]
036:         lea     rsi, [rbp - 72]
037:         mov     qword ptr [rbp - 88], rsi
038:         call    swift_beginAccess@PLT
039:         mov     rsi, qword ptr [rbp - 96]
040:         mov     rdi, qword ptr [rbp - 88]
041:         mov     qword ptr [r13 + 24], rsi
042:         call    swift_endAccess@PLT
043:         mov     rax, qword ptr [rbp - 80]
044:         add     rsp, 144
045:         pop     rbp
046:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: スタックポインタから144バイト引く
005: ベースポインタから80バイト引いたアドレスにr13レジスタの値(=self)をセット
006: ベースポインタから96バイト引いたアドレスにrsiレジスタの値(=第2引数=value2)をセット
007: ベースポインタから136バイト引いたアドレスにrdiレジスタの値(=第1引数=value1)をセット
008: ベースポインタから8バイト引いたアドレスに0をセット
009: ベースポインタから16バイト引いたアドレスに0をセット
010: ベースポインタから24バイト引いたアドレスに0をセット
011: ベースポインタから8バイト引いたアドレスにrdiレジスタの値(=第1引数=value1)をセット
012: ベースポインタから16バイト引いたアドレスにrsiレジスタの値(=第2引数=value2)をセット
013: ベースポインタから24バイト引いたアドレスにr13レジスタの値(=self)をセット
014: rdiレジスタにr13レジスタの値(=self)をセット
015: rdiレジスタに16バイトを加算(→TestData.value1のアドレスになる)
016: r13レジスタの値(=self)へ16バイト足したアドレス(=TestData.value1のアドレス)に0をセット
017: raxレジスタにr13レジスタの値(=self)をセット
018: raxレジスタに24バイトを加算(→TestData.value2のアドレスになる)
019: ベースポインタから120バイト引いたアドレスにraxレジスタの値(=TestData.value2のアドレス)をセット
020: r13レジスタに24バイト加算したアドレス(=TestData.value2のアドレス)に0をセット
021: eaxレジスタの値同士でxorする。意味合い的にはeaxのゼロクリア
022: ecxレジスタにeaxレジスタの値(=0)をセット
023: ベースポインタから104バイト引いたアドレスにrcxレジスタの値(=0)をセット
024: rsiレジスタにベースポインタから48バイト引いたアドレス(=ValueBufferのポインタ)をセット
025: ベースポインタから128バイト引いたアドレスにrsiレジスタの値(=ValueBufferのポインタ)をセット
026: edxレジスタに33(=0x21、ExclusivityFlags::Tracking+Modify)をセット
027: ベースポインタから112バイト引いたアドレスにrdxレジスタの値(=33)をセット
028: swift_beginAccessを呼ぶ
029: raxレジスタにベースポインタから136バイト引いたアドレスの値(=value1)をセット
030: rdiレジスタにベースポインタから128バイト引いたアドレスの値(=ValueBufferのポインタ)をセット
031: r13レジスタに16バイト加算したアドレス(=TestData.value1のアドレス)にraxレジスタの値(=value1)をセット
032: swift_endAccessを呼ぶ
033: rdiレジスタにベースポインタから120バイト引いたアドレスの値(=TestData.value2のアドレス)をセット
034: rdxレジスタにベースポインタから112バイト引いたアドレスの値(=0x21)をセット
035: rcxレジスタにベースポインタから104バイト引いたアドレスの値(=0)をセット
036: rsiレジスタにベースポインタから72バイト引いたアドレス(=ValueBufferのポインタ)をセット
037: ベースポインタから88バイト引いたアドレスにrsiレジスタの値(=ValueBufferのポインタ)をセット
038: swift_beginAccessを呼ぶ
039: rsiレジスタにベースポインタから96バイト引いたアドレスの値(=value2)をセット
040: rdiレジスタにベースポインタから88バイト引いたアドレスの値(=ValueBufferのポインタ)をセット
041: r13レジスタに24バイト加算したアドレス(=TestData.value2のアドレス)にrsiレジスタの値(=value2)をセット
042: swift_endAccessを呼ぶ
043: raxレジスタにベースポインタから80バイト引いたアドレスの値(=self)をセット
044: スタックポインタに144バイト足す
045: スタックに待避してあったベースポインタを復元
046: リターン

005の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-144 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-144〜rbp-137
rsp+8〜rsp+15 rbp-136〜rbp-129 value1
rsp+16〜rsp+23 rbp-128〜rbp-121 1個目のswift_beginAccess用2番目の引数(ValueBufferのポインタ)
rsp+24〜rsp+31 rbp-120〜rbp-113 1個目のswift_beginAccess用1番目の引数(TestData.value2のアドレス)
rsp+32〜rsp+39 rbp-112〜rbp-105 1個目のswift_beginAccess用3番目の引数(=0x21)
rsp+40〜rsp+47 rbp-104〜rbp-97 1個目のswift_beginAccess用4番目の引数(=0)
rsp+48〜rsp+55 rbp-96〜rbp-89 value2
rsp+56〜rsp+63 rbp-88〜rbp-81 2個目のswift_beginAccess用2番目の引数(ValueBufferのポインタ)
rsp+64〜rsp+71 rbp-80〜rbp-73 self
rsp+72〜rsp+79 rbp-72〜rbp-65 2個目のswift_beginAccess用ValueBuffer
rsp+80〜rsp+87 rbp-64〜rbp-57
rsp+88〜rsp+95 rbp-56〜rbp-49
rsp+96〜rsp+103 rbp-48〜rbp-41 1個目のswift_beginAccess用ValueBuffer
rsp+104〜rsp+111 rbp-40〜rbp-33
rsp+112〜rsp+119 rbp-32〜rbp-25
rsp+120〜rsp+127 rbp-24〜rbp-17 self
rsp+128〜rsp+135 rbp-16〜rbp-9 value2
rsp+136〜rsp+143 rbp-8〜rbp-1 value1
rsp+144 rbp ベースポインタ
rsp+144〜rsp+151 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

長いですが、SIL同様、特別なことはしていません。

kabeyakabeya

最後、testFuncを見てみます。

001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 56
006:         mov     r13, rdi
007:         mov     qword ptr [rbp - 16], 0
008:         mov     qword ptr [rbp - 16], r13
009:         mov     rax, qword ptr [r13]
010:         mov     rax, qword ptr [rax + 96]
011:         call    rax
012:         mov     qword ptr [rbp - 56], rax
013:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
014:         xor     eax, eax
015:         mov     ecx, eax
016:         lea     rsi, [rbp - 40]
017:         mov     qword ptr [rbp - 48], rsi
018:         mov     edx, 33
019:         call    swift_beginAccess@PLT
020:         mov     rax, qword ptr [rbp - 56]
021:         mov     rdi, qword ptr [rbp - 48]
022:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
023:         call    swift_endAccess@PLT
024:         add     rsp, 56
025:         pop     r13
026:         pop     rbp
027:         ret

002: 現在のベースポインタをスタックに待避
003: ベースポインタにスタックポインタの現在の値をセット
004: r13レジスタの値をスタックに待避。r13にはselfが入っている
005: スタックポインタから56バイト引く
006: r13レジスタ(=self)にrdiレジスタの値(=第1引数=TestDataのポインタ)をセット
007: ベースポインタから16バイト引いたアドレスに0をセット
008: ベースポインタから16バイト引いたアドレスにr13レジスタの値(=TestDataのポインタ)をセット
009: raxレジスタにr13レジスタの指すアドレスの値(=TestDataの中身の最初の8バイト=isa)をセット
010: raxレジスタにraxレジスタの指すアドレス+96バイトのアドレスの値(=TestData.value2.getterの関数ポインタ)をセット
011: TestData.value2.getterを呼ぶ
012: ベースポインタから56バイト引いたアドレスにraxレジスタの値(=TestData.value2.getterの返値)をセット
013: rdiレジスタにglobalValue変数のアドレスをセット
014: eaxレジスタの値同士でxorする。意味合い的にはeaxのゼロクリア
015: ecxレジスタにeaxレジスタの値(=0)をセット
016: rsiレジスタにベースポインタから40バイト引いたアドレス(=ValueBufferのポインタ)をセット
017: ベースポインタから48バイト引いたアドレスにrsiレジスタの値(=ValueBufferのポインタ)をセット
018: edxレジスタに33(=0x21)をセット
019: swift_beginAccessを呼ぶ
020: raxレジスタにベースポインタから56バイト引いたアドレスの値(=TestData.value2)をセット
021: rdiレジスタにベースポインタから48バイト引いたアドレスの値(=ValueBufferのポインタ)をセット
022: globalValue変数のアドレスにraxレジスタの値(=TestData.value2)をセット
023: swift_endAccessを呼ぶ
024: スタックポインタに56バイト足す
025: スタックに待避してあったr13を復元
026: スタックに待避してあったベースポインタを復元
027: リターン

005の時点でスタックのメモリは以下のようになっています。

アドレス(rspベース) アドレス(rbpベース) 内容 備考
rsp rbp-64 スタックポインタ アドレスの小さいほう
rsp+0〜rsp+7 rbp-64〜rbp-57
rsp+8〜rsp+15 rbp-56〜rbp-49 TestData.value2
rsp+16〜rsp+23 rbp-48〜rbp-41 swift_beginAccess用2番目の引数(ValueBufferのポインタ)
rsp+24〜rsp+31 rbp-40〜rbp-33 swift_beginAccess用ValueBuffer
rsp+32〜rsp+39 rbp-32〜rbp-25
rsp+40〜rsp+47 rbp-24〜rbp-17
rsp+48〜rsp+55 rbp-16〜rbp-9 TestDataのポインタ
rsp+56〜rsp+31 rbp-8〜rbp-1 元のself
rsp+64 rbp ベースポインタ
rsp+64〜rsp+71 rbp〜rbp+7 元のベースポインタ(戻り先) アドレスの大きいほう

SIL同様、参照カウンタの操作はしていません。

kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thick
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒にselfもレジスタに入れて渡す。
  • 渡す前に参照をretainする
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では参照カウンタの操作はしない
  • 呼び出し後、呼ぶ前にretainした分のreleaseをする

こんな感じでしょうか。

kabeyakabeya

5個目の調査

5個目の調査は、letにしてみます。

  • thick
  • var → let
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし
class TestData {
    var value1: Int = 0
    var value2: Int = 0
    
    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

var globalValue: Int = 0

func testFunc(_ testData: TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    let testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}

classの場合letでもvarでも差がない気はしますが、どうなるでしょうか。

kabeyakabeya

SIL

SILを見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test58mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = metatype $@thick TestData.Type             // user: %6
005:   %1 = integer_literal $Builtin.Int64, 3          // user: %2
006:   %2 = struct $Int (%1 : $Builtin.Int64)          // user: %6
007:   %3 = integer_literal $Builtin.Int64, 4          // user: %4
008:   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %6
009:   // function_ref TestData.__allocating_init(value1:value2:)
010:   %5 = function_ref @$s5Test58TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %6
011:   %6 = apply %5(%2, %4, %0) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // users: %10, %7, %9
012:   debug_value %6 : $TestData, let, name "testData" // id: %7
013:   // function_ref testFunc(_:)
014:   %8 = function_ref @$s5Test58testFuncyyAA8TestDataCF : $@convention(thin) (@guaranteed TestData) -> () // user: %9
015:   %9 = apply %8(%6) : $@convention(thin) (@guaranteed TestData) -> ()
016:   strong_release %6 : $TestData                   // id: %10
017:   %11 = tuple ()                                  // user: %12
018:   return %11 : $()                                // id: %12
019: } // end sil function '$s5Test58mainFuncyyF'

変わったところだけ見ていきます。

  • alloc_stack/dealloc_stackおよびstoreがなくなっている。varでなくletになったことで、メモリ確保、メモリへの明示的なロードがなくなった
  • strong_retain/destroy_addrもなくなっている

前者は、structのときのvarletでも起こったことです。ポインタ変数でも同じことなんですね。
また、後者が予想外でした。参照型はメンバ関数にmutatingのキーワードがなく、letで渡してもプロパティを更新できます。それでもretain/releaseが減るんですね。
と言いますかvarのときにretain/releaseのあるのが予想外なのかも知れませんけども。

kabeyakabeya

次いで、TestData.init(value1:value2:)を見ます。

001: // TestData.__allocating_init(value1:value2:)
002: sil hidden [exact_self_class] @$s5Test58TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData {
003: // %0 "value1"                                    // user: %5
004: // %1 "value2"                                    // user: %5
005: // %2 "$metatype"
006: bb0(%0 : $Int, %1 : $Int, %2 : $@thick TestData.Type):
007:   %3 = alloc_ref $TestData                        // user: %5
008:   // function_ref TestData.init(value1:value2:)
009:   %4 = function_ref @$s5Test58TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %5
010:   %5 = apply %4(%0, %1, %3) : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %6
011:   return %5 : $TestData                           // id: %6
012: } // end sil function '$s5Test58TestDataC6value16value2ACSi_SitcfC'
013: 
014: // TestData.init(value1:value2:)
015: sil hidden @$s5Test58TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData {
016: // %0 "value1"                                    // users: %16, %3
017: // %1 "value2"                                    // users: %20, %4
018: // %2 "self"                                      // users: %18, %14, %10, %6, %22, %5
019: bb0(%0 : $Int, %1 : $Int, %2 : $TestData):
020:   debug_value %0 : $Int, let, name "value1", argno 1 // id: %3
021:   debug_value %1 : $Int, let, name "value2", argno 2 // id: %4
022:   debug_value %2 : $TestData, let, name "self", argno 3, implicit // id: %5
023:   %6 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %9
024:   %7 = integer_literal $Builtin.Int64, 0          // user: %8
025:   %8 = struct $Int (%7 : $Builtin.Int64)          // user: %9
026:   store %8 to %6 : $*Int                          // id: %9
027:   %10 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %13
028:   %11 = integer_literal $Builtin.Int64, 0         // user: %12
029:   %12 = struct $Int (%11 : $Builtin.Int64)        // user: %13
030:   store %12 to %10 : $*Int                        // id: %13
031:   %14 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %15
032:   %15 = begin_access [modify] [dynamic] %14 : $*Int // users: %16, %17
033:   store %0 to %15 : $*Int                         // id: %16
034:   end_access %15 : $*Int                          // id: %17
035:   %18 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %19
036:   %19 = begin_access [modify] [dynamic] %18 : $*Int // users: %20, %21
037:   store %1 to %19 : $*Int                         // id: %20
038:   end_access %19 : $*Int                          // id: %21
039:   return %2 : $TestData                           // id: %22
040: } // end sil function '$s5Test58TestDataC6value16value2ACSi_Sitcfc'

こちらは完全にvarのときと同じです。

kabeyakabeya

最後、testFuncを見てみます。

001: // testFunc(_:)
002: sil hidden @$s5Test58testFuncyyAA8TestDataCF : $@convention(thin) (@guaranteed TestData) -> () {
003: // %0 "testData"                                  // users: %4, %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s5Test511globalValueSivp : $*Int // user: %5
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = class_method %0 : $TestData, #TestData.value2!getter : (TestData) -> () -> Int, $@convention(method) (@guaranteed TestData) -> Int // user: %4
008:   %4 = apply %3(%0) : $@convention(method) (@guaranteed TestData) -> Int // user: %6
009:   %5 = begin_access [modify] [dynamic] %1 : $*Int // users: %6, %7
010:   store %4 to %5 : $*Int                          // id: %6
011:   end_access %5 : $*Int                           // id: %7
012:   %8 = tuple ()                                   // user: %9
013:   return %8 : $()                                 // id: %9
014: } // end sil function '$s5Test58testFuncyyAA8TestDataCF'

こちらも完全にvarのときと同じです。

kabeyakabeya

アセンブラ

アセンブラを順に見ていきます。まずmainFuncから。

001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 16], 0
007:         xor     eax, eax
008:         mov     edi, eax
009:         call    (type metadata accessor for output.TestData)
010:         mov     r13, rax
011:         mov     edi, 3
012:         mov     esi, 4
013:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
014:         mov     rdi, rax
015:         mov     qword ptr [rbp - 24], rdi
016:         mov     qword ptr [rbp - 16], rdi
017:         call    (output.testFunc(output.TestData) -> ())
018:         mov     rdi, qword ptr [rbp - 24]
019:         call    swift_release@PLT
020:         add     rsp, 24
021:         pop     r13
022:         pop     rbp
023:         ret

単純に、swift_retainswift_releaseの呼び出しだけがなくなっています。それ以外は同じです。
SILはスタックの変数の確保に違いがありましたが、アセンブラになるとその部分の差はなくなります。structvarletの場合も、SILだけ差があってアセンブラは完全に一致していましたので、これは予想通りとも言えます。

kabeyakabeya

次はTestData.__allocating_init(value1:value2:)TestData.init(value1:value2:)を見てみます。
まずTestData.__allocating_init(value1:value2:)です。

001: output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 32], r13
007:         mov     qword ptr [rbp - 16], rsi
008:         mov     rax, rdi
009:         mov     rdi, qword ptr [rbp - 32]
010:         mov     qword ptr [rbp - 24], rax
011:         mov     esi, 32
012:         mov     edx, 7
013:         call    swift_allocObject@PLT
014:         mov     rdi, qword ptr [rbp - 24]
015:         mov     rsi, qword ptr [rbp - 16]
016:         mov     r13, rax
017:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
018:         add     rsp, 24
019:         pop     r13
020:         pop     rbp
021:         ret

完全にvarのときと同じです。

ついでTestData.init(value1:value2:)です。

001: output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 144
005:         mov     qword ptr [rbp - 80], r13
006:         mov     qword ptr [rbp - 96], rsi
007:         mov     qword ptr [rbp - 136], rdi
008:         mov     qword ptr [rbp - 8], 0
009:         mov     qword ptr [rbp - 16], 0
010:         mov     qword ptr [rbp - 24], 0
011:         mov     qword ptr [rbp - 8], rdi
012:         mov     qword ptr [rbp - 16], rsi
013:         mov     qword ptr [rbp - 24], r13
014:         mov     rdi, r13
015:         add     rdi, 16
016:         mov     qword ptr [r13 + 16], 0
017:         mov     rax, r13
018:         add     rax, 24
019:         mov     qword ptr [rbp - 120], rax
020:         mov     qword ptr [r13 + 24], 0
021:         xor     eax, eax
022:         mov     ecx, eax
023:         mov     qword ptr [rbp - 104], rcx
024:         lea     rsi, [rbp - 48]
025:         mov     qword ptr [rbp - 128], rsi
026:         mov     edx, 33
027:         mov     qword ptr [rbp - 112], rdx
028:         call    swift_beginAccess@PLT
029:         mov     rax, qword ptr [rbp - 136]
030:         mov     rdi, qword ptr [rbp - 128]
031:         mov     qword ptr [r13 + 16], rax
032:         call    swift_endAccess@PLT
033:         mov     rdi, qword ptr [rbp - 120]
034:         mov     rdx, qword ptr [rbp - 112]
035:         mov     rcx, qword ptr [rbp - 104]
036:         lea     rsi, [rbp - 72]
037:         mov     qword ptr [rbp - 88], rsi
038:         call    swift_beginAccess@PLT
039:         mov     rsi, qword ptr [rbp - 96]
040:         mov     rdi, qword ptr [rbp - 88]
041:         mov     qword ptr [r13 + 24], rsi
042:         call    swift_endAccess@PLT
043:         mov     rax, qword ptr [rbp - 80]
044:         add     rsp, 144
045:         pop     rbp
046:         ret

こちらもvarと完全に同じです。

kabeyakabeya

最後、testFuncを見てみます。

001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 56
006:         mov     r13, rdi
007:         mov     qword ptr [rbp - 16], 0
008:         mov     qword ptr [rbp - 16], r13
009:         mov     rax, qword ptr [r13]
010:         mov     rax, qword ptr [rax + 96]
011:         call    rax
012:         mov     qword ptr [rbp - 56], rax
013:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
014:         xor     eax, eax
015:         mov     ecx, eax
016:         lea     rsi, [rbp - 40]
017:         mov     qword ptr [rbp - 48], rsi
018:         mov     edx, 33
019:         call    swift_beginAccess@PLT
020:         mov     rax, qword ptr [rbp - 56]
021:         mov     rdi, qword ptr [rbp - 48]
022:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
023:         call    swift_endAccess@PLT
024:         add     rsp, 56
025:         pop     r13
026:         pop     rbp
027:         ret

これもvarと同じです。

kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thick
  • var → let
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒にselfもレジスタに入れて渡す。
  • varのときと異なり、渡す前に参照をretainしない
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では参照カウンタの操作はしない
  • 呼び出し後、呼ぶ前にretainしていないので余分なreleaseはしない。イニシャライザで返ってきた時点の分のreleaseのみ
kabeyakabeya

6個目の調査

6個目の調査は、イニシャライザでの動作を見ます。

  • thick
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし
class TestData {
    var value1: Int = 0
    var value2: Int = 0

    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

struct Foo {
    var propValue: Int = 0
    
    init(_ testData: TestData) {
        self.propValue = testData.value2
    }
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    var foo = Foo(testData)
    foo.propValue = 5
}
kabeyakabeya

SIL

これのSILを順に見ていきます。まずmainFuncから。

001: // mainFunc()
002: sil hidden @$s5Test68mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %9, %23, %22
005:   %1 = metatype $@thick TestData.Type             // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.__allocating_init(value1:value2:)
011:   %6 = function_ref @$s5Test68TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // users: %9, %13, %8
013:   strong_retain %7 : $TestData                    // id: %8
014:   store %7 to %0 : $*TestData                     // id: %9
015:   %10 = alloc_stack $Foo, var, name "foo"         // users: %14, %21, %17
016:   %11 = metatype $@thin Foo.Type                  // user: %13
017:   // function_ref Foo.init(_:)
018:   %12 = function_ref @$s5Test63FooVyAcA8TestDataCcfC : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo // user: %13
019:   %13 = apply %12(%7, %11) : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo // user: %14
020:   store %13 to %10 : $*Foo                        // id: %14
021:   %15 = integer_literal $Builtin.Int64, 5         // user: %16
022:   %16 = struct $Int (%15 : $Builtin.Int64)        // user: %19
023:   %17 = begin_access [modify] [static] %10 : $*Foo // users: %20, %18
024:   %18 = struct_element_addr %17 : $*Foo, #Foo.propValue // user: %19
025:   store %16 to %18 : $*Int                        // id: %19
026:   end_access %17 : $*Foo                          // id: %20
027:   dealloc_stack %10 : $*Foo                       // id: %21
028:   destroy_addr %0 : $*TestData                    // id: %22
029:   dealloc_stack %0 : $*TestData                   // id: %23
030:   %24 = tuple ()                                  // user: %25
031:   return %24 : $()                                // id: %25
032: } // end sil function '$s5Test68mainFuncyyF'

全ステップ読み解くのがだんだんつらくなっているので、特徴的なポイントだけ列挙します。

  1. TestData.init(value1:value2:)から返ってきた参照(参照カウント1)を013でstrong_retainしている(参照カウント1→2)
  2. 028でdestroy_addrしている(参照カウントが1つ減る)
  3. varを普通の関数に渡していた)4個目の調査のときにはdestory_addrの直前に存在したstrong_releaseがなくなっている

つまりここだけ見るとリークするんですね。この関数自体では参照カウントが1余っているんです。

kabeyakabeya

ついでTestData.init(value1:value2:)を見てみます。

001: // TestData.__allocating_init(value1:value2:)
002: sil hidden [exact_self_class] @$s5Test68TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData {
003: // %0 "value1"                                    // user: %5
004: // %1 "value2"                                    // user: %5
005: // %2 "$metatype"
006: bb0(%0 : $Int, %1 : $Int, %2 : $@thick TestData.Type):
007:   %3 = alloc_ref $TestData                        // user: %5
008:   // function_ref TestData.init(value1:value2:)
009:   %4 = function_ref @$s5Test68TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %5
010:   %5 = apply %4(%0, %1, %3) : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData // user: %6
011:   return %5 : $TestData                           // id: %6
012: } // end sil function '$s5Test68TestDataC6value16value2ACSi_SitcfC'
013: 
014: // TestData.init(value1:value2:)
015: sil hidden @$s5Test68TestDataC6value16value2ACSi_Sitcfc : $@convention(method) (Int, Int, @owned TestData) -> @owned TestData {
016: // %0 "value1"                                    // users: %16, %3
017: // %1 "value2"                                    // users: %20, %4
018: // %2 "self"                                      // users: %18, %14, %10, %6, %22, %5
019: bb0(%0 : $Int, %1 : $Int, %2 : $TestData):
020:   debug_value %0 : $Int, let, name "value1", argno 1 // id: %3
021:   debug_value %1 : $Int, let, name "value2", argno 2 // id: %4
022:   debug_value %2 : $TestData, let, name "self", argno 3, implicit // id: %5
023:   %6 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %9
024:   %7 = integer_literal $Builtin.Int64, 0          // user: %8
025:   %8 = struct $Int (%7 : $Builtin.Int64)          // user: %9
026:   store %8 to %6 : $*Int                          // id: %9
027:   %10 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %13
028:   %11 = integer_literal $Builtin.Int64, 0         // user: %12
029:   %12 = struct $Int (%11 : $Builtin.Int64)        // user: %13
030:   store %12 to %10 : $*Int                        // id: %13
031:   %14 = ref_element_addr %2 : $TestData, #TestData.value1 // user: %15
032:   %15 = begin_access [modify] [dynamic] %14 : $*Int // users: %16, %17
033:   store %0 to %15 : $*Int                         // id: %16
034:   end_access %15 : $*Int                          // id: %17
035:   %18 = ref_element_addr %2 : $TestData, #TestData.value2 // user: %19
036:   %19 = begin_access [modify] [dynamic] %18 : $*Int // users: %20, %21
037:   store %1 to %19 : $*Int                         // id: %20
038:   end_access %19 : $*Int                          // id: %21
039:   return %2 : $TestData                           // id: %22
040: } // end sil function '$s5Test68TestDataC6value16value2ACSi_Sitcfc'

これに関しては4個目、5個目の調査と完全に同じです。

kabeyakabeya

続いてFoo.init(_:)です。

001: // Foo.init(_:)
002: sil hidden @$s5Test63FooVyAcA8TestDataCcfC : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo {
003: // %0 "testData"                                  // users: %15, %8, %9, %3
004: // %1 "$metatype"
005: bb0(%0 : $TestData, %1 : $@thin Foo.Type):
006:   %2 = alloc_stack $Foo, var, name "self", implicit // users: %7, %10, %16
007:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %3
008:   %4 = integer_literal $Builtin.Int64, 0          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
010:   %6 = struct $Foo (%5 : $Int)                    // user: %7
011:   store %6 to %2 : $*Foo                          // id: %7
012:   %8 = class_method %0 : $TestData, #TestData.value2!getter : (TestData) -> () -> Int, $@convention(method) (@guaranteed TestData) -> Int // user: %9
013:   %9 = apply %8(%0) : $@convention(method) (@guaranteed TestData) -> Int // users: %12, %14
014:   %10 = begin_access [modify] [static] %2 : $*Foo // users: %13, %11
015:   %11 = struct_element_addr %10 : $*Foo, #Foo.propValue // user: %12
016:   store %9 to %11 : $*Int                         // id: %12
017:   end_access %10 : $*Foo                          // id: %13
018:   %14 = struct $Foo (%9 : $Int)                   // user: %17
019:   strong_release %0 : $TestData                   // id: %15
020:   dealloc_stack %2 : $*Foo                        // id: %16
021:   return %14 : $Foo                               // id: %17
022: } // end sil function '$s5Test63FooVyAcA8TestDataCcfC'

3個目の調査との違いは以下の2点です。

  1. .value2struct_extractではなく、TestData.value2.getter呼び出しでの取得になっている
  2. 019でtestDatastrong_releaseしている

2については、mainFunc()で1余っていたように見えた参照カウントが、ここでstrong_releaseにより1減っているんですね。
これによってmainFunc()の終わりのdestroy_addrで参照カウントが0になり、解放されるということになります。

通常関数とイニシャライザでは、参照カウンタの操作が違うということが分かります。

kabeyakabeya

次はTestData.__allocating_init(value1:value2:)TestData.init(value1:value2:)ですが、結果から言うと4個目、5個目の調査と同じなので閉じておきます。

`TestData.__allocating_init(value1:value2:)`、`TestData.init(value1:value2:)`のアセンブラ
001: output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 32], r13
007:         mov     qword ptr [rbp - 16], rsi
008:         mov     rax, rdi
009:         mov     rdi, qword ptr [rbp - 32]
010:         mov     qword ptr [rbp - 24], rax
011:         mov     esi, 32
012:         mov     edx, 7
013:         call    swift_allocObject@PLT
014:         mov     rdi, qword ptr [rbp - 24]
015:         mov     rsi, qword ptr [rbp - 16]
016:         mov     r13, rax
017:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
018:         add     rsp, 24
019:         pop     r13
020:         pop     rbp
021:         ret
001: output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 144
005:         mov     qword ptr [rbp - 80], r13
006:         mov     qword ptr [rbp - 96], rsi
007:         mov     qword ptr [rbp - 136], rdi
008:         mov     qword ptr [rbp - 8], 0
009:         mov     qword ptr [rbp - 16], 0
010:         mov     qword ptr [rbp - 24], 0
011:         mov     qword ptr [rbp - 8], rdi
012:         mov     qword ptr [rbp - 16], rsi
013:         mov     qword ptr [rbp - 24], r13
014:         mov     rdi, r13
015:         add     rdi, 16
016:         mov     qword ptr [r13 + 16], 0
017:         mov     rax, r13
018:         add     rax, 24
019:         mov     qword ptr [rbp - 120], rax
020:         mov     qword ptr [r13 + 24], 0
021:         xor     eax, eax
022:         mov     ecx, eax
023:         mov     qword ptr [rbp - 104], rcx
024:         lea     rsi, [rbp - 48]
025:         mov     qword ptr [rbp - 128], rsi
026:         mov     edx, 33
027:         mov     qword ptr [rbp - 112], rdx
028:         call    swift_beginAccess@PLT
029:         mov     rax, qword ptr [rbp - 136]
030:         mov     rdi, qword ptr [rbp - 128]
031:         mov     qword ptr [r13 + 16], rax
032:         call    swift_endAccess@PLT
033:         mov     rdi, qword ptr [rbp - 120]
034:         mov     rdx, qword ptr [rbp - 112]
035:         mov     rcx, qword ptr [rbp - 104]
036:         lea     rsi, [rbp - 72]
037:         mov     qword ptr [rbp - 88], rsi
038:         call    swift_beginAccess@PLT
039:         mov     rsi, qword ptr [rbp - 96]
040:         mov     rdi, qword ptr [rbp - 88]
041:         mov     qword ptr [r13 + 24], rsi
042:         call    swift_endAccess@PLT
043:         mov     rax, qword ptr [rbp - 80]
044:         add     rsp, 144
045:         pop     rbp
046:         ret
kabeyakabeya

次はFoo.init(_:)です。

001: output.Foo.init(output.TestData) -> output.Foo:
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 40
006:         mov     r13, rdi
007:         mov     qword ptr [rbp - 40], r13
008:         mov     qword ptr [rbp - 16], 0
009:         mov     qword ptr [rbp - 24], 0
010:         mov     qword ptr [rbp - 24], r13
011:         mov     qword ptr [rbp - 16], 0
012:         mov     rax, qword ptr [r13]
013:         mov     rax, qword ptr [rax + 96]
014:         call    rax
015:         mov     rdi, qword ptr [rbp - 40]
016:         mov     qword ptr [rbp - 32], rax
017:         mov     qword ptr [rbp - 16], rax
018:         call    swift_release@PLT
019:         mov     rax, qword ptr [rbp - 32]
020:         add     rsp, 40
021:         pop     r13
022:         pop     rbp
023:         ret

3個目のときとの違いは以下です。

  1. testData.value2を取得するのに、TestData.value2.getterの関数呼び出しを014で行っている
  2. 関数呼び出しはoverrideがあるので、013の関数テーブル(rax+96)経由で行っている
  3. 018でswift_releaseしている
  4. 関数内に、上記の2つの関数呼び出しがあるのでスタックポインタをセットしている
kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thick
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒にselfもレジスタに入れて渡す
  • 渡す前に参照をretainする
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先(イニシャライザ)では参照をreleaseする
  • 呼ぶ前にretainしているが、呼び出し後にその分のreleaseはしない。イニシャライザで返ってきた時点の分のreleaseのみ
kabeyakabeya

7個目の調査

structStringを入れてみます。

  • thick?
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし
struct TestData {
    var value1: String = ""
    var value2: Int = 0
}

var globalValue: Int = 0

func testFunc(_ testData: TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: "3", value2: 4)
    testFunc(testData)
}
kabeyakabeya

ここからは少し簡略化して書きます。

SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s5Test78mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack $TestData, var, name "testData" // users: %13, %18, %17
005:   %1 = metatype $@thin TestData.Type              // user: %11
006:   %2 = string_literal utf8 "3"                    // user: %7
007:   %3 = integer_literal $Builtin.Word, 1           // user: %7
008:   %4 = integer_literal $Builtin.Int1, -1          // user: %7
009:   %5 = metatype $@thin String.Type                // user: %7
010:   // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
011:   %6 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %7
012:   %7 = apply %6(%2, %3, %4, %5) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %11
013:   %8 = integer_literal $Builtin.Int64, 4          // user: %9
014:   %9 = struct $Int (%8 : $Builtin.Int64)          // user: %11
015:   // function_ref TestData.init(value1:value2:)
016:   %10 = function_ref @$s5Test78TestDataV6value16value2ACSS_SitcfC : $@convention(method) (@owned String, Int, @thin TestData.Type) -> @owned TestData // user: %11
017:   %11 = apply %10(%7, %9, %1) : $@convention(method) (@owned String, Int, @thin TestData.Type) -> @owned TestData // users: %16, %13, %15, %12
018:   retain_value %11 : $TestData                    // id: %12
019:   store %11 to %0 : $*TestData                    // id: %13
020:   // function_ref testFunc(_:)
021:   %14 = function_ref @$s5Test78testFuncyyAA8TestDataVF : $@convention(thin) (@guaranteed TestData) -> () // user: %15
022:   %15 = apply %14(%11) : $@convention(thin) (@guaranteed TestData) -> ()
023:   release_value %11 : $TestData                   // id: %16
024:   destroy_addr %0 : $*TestData                    // id: %17
025:   dealloc_stack %0 : $*TestData                   // id: %18
026:   %19 = tuple ()                                  // user: %20
027:   return %19 : $()                                // id: %20
028: } // end sil function '$s5Test78mainFuncyyF'

mainFunc()のSILの特徴は以下です。

  1. 018でretain_valueという呼び出しが発生
  2. 同様に023でrelease_valueという呼び出しが発生
  3. 024ではIntだけのstructのときにはなかったdestroy_addrも発生
  4. 021の関数ポインタの引数が@guaranteedで修飾されている
SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s5Test78testFuncyyAA8TestDataVF : $@convention(thin) (@guaranteed TestData) -> () {
003: // %0 "testData"                                  // users: %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s5Test711globalValueSivp : $*Int // user: %4
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = struct_extract %0 : $TestData, #TestData.value2 // user: %5
008:   %4 = begin_access [modify] [dynamic] %1 : $*Int // users: %5, %6
009:   store %3 to %4 : $*Int                          // id: %5
010:   end_access %4 : $*Int                           // id: %6
011:   %7 = tuple ()                                   // user: %8
012:   return %7 : $()                                 // id: %8
013: } // end sil function '$s5Test78testFuncyyAA8TestDataVF'

testFunc()のSILは特に目立った特徴がありませんでした。

kabeyakabeya
アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 64
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 32], xmm0
007:         mov     qword ptr [rbp - 16], 0
008:         lea     rdi, [rip + .L.str.1.3]
009:         mov     esi, 1
010:         mov     edx, 1
011:         call    ($sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC)@PLT
012:         mov     rdi, rax
013:         mov     rsi, rdx
014:         mov     edx, 4
015:         call    (output.TestData.init(value1: Swift.String, value2: Swift.Int) -> output.TestData)
016:         mov     qword ptr [rbp - 56], rax
017:         mov     rdi, rdx
018:         mov     qword ptr [rbp - 40], rdi
019:         mov     qword ptr [rbp - 48], rcx
020:         call    swift_bridgeObjectRetain@PLT
021:         mov     rdi, qword ptr [rbp - 56]
022:         mov     rdx, qword ptr [rbp - 48]
023:         mov     rsi, qword ptr [rbp - 40]
024:         mov     qword ptr [rbp - 32], rdi
025:         mov     qword ptr [rbp - 24], rsi
026:         mov     qword ptr [rbp - 16], rdx
027:         call    (output.testFunc(output.TestData) -> ())
028:         mov     rdi, qword ptr [rbp - 40]
029:         call    swift_bridgeObjectRelease@PLT
030:         lea     rdi, [rbp - 32]
031:         call    (outlined destroy of output.TestData)
032:         add     rsp, 64
033:         pop     rbp
034:         ret

mainFunc()のアセンブラで特徴的な箇所は以下になります。

  1. 020でswift_bridgeObjectRetainというのが呼ばれている
  2. 同様に029でswift_bridgeObjectReleaseというのが呼ばれている
  3. 031で(outlined destory of output.TestData)というのが呼ばれている

swift_bridgeObjectRetainというのは別のスクラップで詳しく追いかけましたが、ざっくりいうと引数のポインタの指すオブジェクトがObjective-CのオブジェクトなのかSwiftのオブジェクトなのか分からない場合でも、適切にretainする、という関数です。
swift_bridgeObjectReleaseはそれのreleaseです。

(outlined destory of output.TestData)は詳しく調べないといけません。

kabeyakabeya

ただその前に、(outlined destory of output.TestData)の引数が何なのかを調べないといけません。
ひとまず、第1引数であるrdiレジスタに何が入っているか確認します。呼び出し地点から逆に追います。

015:         call    (output.TestData.init(value1: Swift.String, value2: Swift.Int) -> output.TestData)
016:         mov     qword ptr [rbp - 56], rax
021:         mov     rdi, qword ptr [rbp - 56]
024:         mov     qword ptr [rbp - 32], rdi
030:         lea     rdi, [rbp - 32]
031:         call    (outlined destroy of output.TestData)
  1. 030で、rbpから32バイト引いたアドレスがrdiレジスタに入れられている
  2. rbpから32バイト引いたアドレスには、024でrdiレジスタの値が入れられている
  3. 024時点のrdiレジスタの値は、021で入れられた、rpbから56バイト引いたアドレスにある値
  4. rpbから56バイト引いたアドレスには016でraxレジスタの値が入れられている
  5. 016時点のraxレジスタの値は、015のTestData.init(value1:value2:)の最初の返値

最終的な値の関係をC言語風に書くと以下のようなことです。

uint64_t data[?] = (uint64_t[?])TestData.init(value1:value2);
rdi = &(data[0]);

TestData.init(value1:value2:)の返値が64ビット値何個かで、rdiレジスタにはその最初の値を入れたメモリのアドレスが入ります。

kabeyakabeya

ではTestData.init(value1:value2:)の最初の返値は何か、ということになります。

001: output.TestData.init(value1: Swift.String, value2: Swift.Int) -> output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         mov     rcx, rdx
005:         mov     rdx, rsi
006:         mov     rax, rdi
007:         pop     rbp
008:         ret

返値としては、rax、rdx、rcxの順に3つのレジスタを使って3個の64ビット値が返っているようです。

  • raxレジスタ←rdiレジスタ(TestData.init(value1:value2:)の第1引数)
  • rdxレジスタ←rsiレジスタ(同第2引数)
  • rcxレジスタ←rdxレジスタ(同第3引数)

です。

TestData.init(value1:value2:)の最初の返値は、TestData.init(value1:value2:)の第1引数そのもの、です。

kabeyakabeya

ということでTestData.init(value1:value2:)の呼び出しの引数は何かをもう1度mainFunc()に戻って確認します。以下の箇所です。

011:         call    ($sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC)@PLT
012:         mov     rdi, rax
013:         mov     rsi, rdx
014:         mov     edx, 4
015:         call    (output.TestData.init(value1: Swift.String, value2: Swift.Int) -> output.TestData)

TestData.init(value1:value2:)の呼び出し時には、以下の引数が設定されていることが分かります。

  • 第1引数(=rdi):$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfCの1つ目の返値rax
  • 第2引数(=rsi):$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfCの2つ目の返値rdi
  • 第3引数(=rdx):4(edxはrdxの下位32ビットについた名前で、edxに代入するとrdxの上位32ビットが0、下位32ビットが代入された値になる)

改めて、$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfCが何を返しているのか調べます。
この関数名はマングル化されていますがデマングルすると、要はString.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)です。

つまり、返っているのはString型なんですね。これは64ビットレジスタを2つ使う型だということです。

kabeyakabeya

Stringのソースコードを見てみます。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/core/String.swift#L353-L385

Stringのメンバ変数はvar guts: _StringGutsだけです。

_StringGutsは以下で定義されています。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/core/StringGuts.swift#L21-L36

_StringGutsのメンバ変数もinternal var _object: _StringObjectだけです。

_StringObjectは以下で定義されています。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/core/StringObject.swift#L20-L198

ちょっと色んなプラットフォーム向けが混在してあれですが、_pointerBitWidth(_64)で見ていくとメンバ変数は以下の2つです。

  • internal var _countAndFlagsBits: UInt64
  • internal var _object: Builtin.BridgeObject

1つ目のメンバ変数は、文字列長となんらかのフラグを持ったフィールドです。2つ目のメンバ変数はObjective-Cオブジェクトとのブリッジをするためのポインタです。swift_bridgeObjectRetainの話でも追いかけていますが、アドレスを直接指しているのではなく、8バイトのうち、最上位4ビットもしくは8ビットと最下位3ビットにフラグが立っています[1]

まとめますと、Stringの最初の返値は、_StringObject_countAndFlagsBitsです。

脚注
  1. このポインタのフラグのうちの上位4ビットが、_StringObjectのコメントに記載されているとおりに使われています ↩︎

kabeyakabeya

というわけで、(outlined destroy of output.TestData)のアセンブラを見ます。

001: outlined destroy of output.TestData:
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         mov     qword ptr [rbp - 8], rdi
006:         mov     rdi, qword ptr [rdi + 8]
007:         call    swift_bridgeObjectRelease@PLT
008:         mov     rax, qword ptr [rbp - 8]
009:         add     rsp, 16
010:         pop     rbp
011:         ret

これが呼び出されている時点で、rdiには_StringObjectの先頭8バイト=_countAndFlagsBitsのアドレスが入っています。

006では、そのアドレスに8バイト足したアドレスから値を取り出してrdiレジスタに入れています。そして007でそれを引数にしてswift_bridgeObjectReleaseを呼んでいます。
_countAndFlagsBitsのアドレスに8バイト足すと、_StringObjectの2個目のメンバであるvar _object: Builtin.BridgeObjectのアドレスになります。
要はこの処理では、Stringが内部で持っている_objectに対してswift_bridgeObjectReleaseをしている、ということです。

また、スルーしていましたがmainFunc()の020のswift_bridgeObjectRetainならびに029のswift_bridgeObjectReleaseも、TestDataのイニシャライザの返値(=Stringのイニシャライザの返値)の2番目の値(rdx)、つまり_objectに対して行っています。

kabeyakabeya

何の話をしてたんでしたっけ、という感じではありますが、次はtestFuncのアセンブラです。

アセンブラ-testFunc()
001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 80
005:         mov     qword ptr [rbp - 72], rdx
006:         xorps   xmm0, xmm0
007:         movaps  xmmword ptr [rbp - 32], xmm0
008:         mov     qword ptr [rbp - 16], 0
009:         mov     qword ptr [rbp - 32], rdi
010:         mov     qword ptr [rbp - 24], rsi
011:         mov     qword ptr [rbp - 16], rdx
012:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
013:         xor     eax, eax
014:         mov     ecx, eax
015:         lea     rsi, [rbp - 56]
016:         mov     qword ptr [rbp - 64], rsi
017:         mov     edx, 33
018:         call    swift_beginAccess@PLT
019:         mov     rdx, qword ptr [rbp - 72]
020:         mov     rdi, qword ptr [rbp - 64]
021:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rdx
022:         call    swift_endAccess@PLT
023:         add     rsp, 80
024:         pop     rbp
025:         ret

SIL同様、testFunc()のアセンブラも特に目立った特徴がありませんでした。

kabeyakabeya

まとめ

今回は以下のケースで調査しました。

  • thick?(struct内にStringを持たせたもの)
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 渡す前にStringの内部のオブジェクトをretainする
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先ではStringの内部のオブジェクトの参照カウンタは操作しない
  • 呼ぶ前にretainしたStringの内部のオブジェクトはreleaseし、さらにイニシャライザで返ってきた時点の分のreleaseもする

要は、structの内部のオブジェクトに参照があれば、classのときと同じような参照カウンタ操作をする、ということです。

kabeyakabeya

8個目の調査

7個目同様、structStringを入れたものでやります。

  • thick?
  • var let
  • 「イニシャライザ 、プロパティセッタ」以外の関数
  • パラメータ修飾子なし
struct TestData {
    var value1: String = ""
    var value2: Int = 0
}

struct Foo {
    var propValue: Int = 0
    
    init(_ testData: TestData) {
        self.propValue = testData.value2
    }
}

func mainFunc() {
    let testData = TestData(value1: "3", value2: 4)
    var foo = Foo(testData)
    foo.propValue = 5
}
SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s5Test88mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = metatype $@thin TestData.Type              // user: %10
005:   %1 = string_literal utf8 "3"                    // user: %6
006:   %2 = integer_literal $Builtin.Word, 1           // user: %6
007:   %3 = integer_literal $Builtin.Int1, -1          // user: %6
008:   %4 = metatype $@thin String.Type                // user: %6
009:   // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
010:   %5 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %6
011:   %6 = apply %5(%1, %2, %3, %4) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %10
012:   %7 = integer_literal $Builtin.Int64, 4          // user: %8
013:   %8 = struct $Int (%7 : $Builtin.Int64)          // user: %10
014:   // function_ref TestData.init(value1:value2:)
015:   %9 = function_ref @$s5Test88TestDataV6value16value2ACSS_SitcfC : $@convention(method) (@owned String, Int, @thin TestData.Type) -> @owned TestData // user: %10
016:   %10 = apply %9(%6, %8, %0) : $@convention(method) (@owned String, Int, @thin TestData.Type) -> @owned TestData // users: %15, %11
017:   debug_value %10 : $TestData, let, name "testData" // id: %11
018:   %12 = alloc_stack $Foo, var, name "foo"         // users: %16, %23, %19
019:   %13 = metatype $@thin Foo.Type                  // user: %15
020:   // function_ref Foo.init(_:)
021:   %14 = function_ref @$s5Test83FooVyAcA8TestDataVcfC : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo // user: %15
022:   %15 = apply %14(%10, %13) : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo // user: %16
023:   store %15 to %12 : $*Foo                        // id: %16
024:   %17 = integer_literal $Builtin.Int64, 5         // user: %18
025:   %18 = struct $Int (%17 : $Builtin.Int64)        // user: %21
026:   %19 = begin_access [modify] [static] %12 : $*Foo // users: %22, %20
027:   %20 = struct_element_addr %19 : $*Foo, #Foo.propValue // user: %21
028:   store %18 to %20 : $*Int                        // id: %21
029:   end_access %19 : $*Foo                          // id: %22
030:   dealloc_stack %12 : $*Foo                       // id: %23
031:   %24 = tuple ()                                  // user: %25
032:   return %24 : $()                                // id: %25
033: } // end sil function '$s5Test88mainFuncyyF'
SIL-Foo.init(_:)
001: // Foo.init(_:)
002: sil hidden @$s5Test83FooVyAcA8TestDataVcfC : $@convention(method) (@owned TestData, @thin Foo.Type) -> Foo {
003: // %0 "testData"                                  // users: %14, %8, %3
004: // %1 "$metatype"
005: bb0(%0 : $TestData, %1 : $@thin Foo.Type):
006:   %2 = alloc_stack $Foo, var, name "self", implicit // users: %7, %9, %15
007:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %3
008:   %4 = integer_literal $Builtin.Int64, 0          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
010:   %6 = struct $Foo (%5 : $Int)                    // user: %7
011:   store %6 to %2 : $*Foo                          // id: %7
012:   %8 = struct_extract %0 : $TestData, #TestData.value2 // users: %11, %13
013:   %9 = begin_access [modify] [static] %2 : $*Foo  // users: %12, %10
014:   %10 = struct_element_addr %9 : $*Foo, #Foo.propValue // user: %11
015:   store %8 to %10 : $*Int                         // id: %11
016:   end_access %9 : $*Foo                           // id: %12
017:   %13 = struct $Foo (%8 : $Int)                   // user: %16
018:   release_value %0 : $TestData                    // id: %14
019:   dealloc_stack %2 : $*Foo                        // id: %15
020:   return %13 : $Foo                               // id: %16
021: } // end sil function '$s5Test83FooVyAcA8TestDataVcfC'
アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 48
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 32], xmm0
007:         mov     qword ptr [rbp - 16], 0
008:         mov     qword ptr [rbp - 40], 0
009:         lea     rdi, [rip + .L.str.1.3]
010:         mov     esi, 1
011:         mov     edx, 1
012:         call    ($sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC)@PLT
013:         mov     rdi, rax
014:         mov     rsi, rdx
015:         mov     edx, 4
016:         call    (output.TestData.init(value1: Swift.String, value2: Swift.Int) -> output.TestData)
017:         mov     rdi, rax
018:         mov     rsi, rdx
019:         mov     rdx, rcx
020:         mov     qword ptr [rbp - 32], rdi
021:         mov     qword ptr [rbp - 24], rsi
022:         mov     qword ptr [rbp - 16], rdx
023:         call    (output.Foo.init(output.TestData) -> output.Foo)
024:         mov     qword ptr [rbp - 40], rax
025:         mov     qword ptr [rbp - 40], 5
026:         add     rsp, 48
027:         pop     rbp
028:         ret
アセンブラ-Foo.init(_:)
001: output.Foo.init(output.TestData) -> output.Foo:
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 48
005:         mov     qword ptr [rbp - 40], rdx
006:         mov     qword ptr [rbp - 48], rsi
007:         mov     rax, rdi
008:         mov     rdi, qword ptr [rbp - 48]
009:         mov     qword ptr [rbp - 8], 0
010:         xorps   xmm0, xmm0
011:         movaps  xmmword ptr [rbp - 32], xmm0
012:         mov     qword ptr [rbp - 16], 0
013:         mov     qword ptr [rbp - 32], rax
014:         mov     qword ptr [rbp - 24], rdi
015:         mov     qword ptr [rbp - 16], rdx
016:         mov     qword ptr [rbp - 8], 0
017:         mov     qword ptr [rbp - 8], rdx
018:         call    swift_bridgeObjectRelease@PLT
019:         mov     rax, qword ptr [rbp - 40]
020:         add     rsp, 48
021:         pop     rbp
022:         ret

まとめ

今回は以下のケースで調査しました。

  • thick?(struct内にStringを持たせたもの)
  • var let
  • 「イニシャライザ 、プロパティセッタ」以外の関数
  • パラメータ修飾子なし

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 渡す前にStringの内部のオブジェクトをretainしない
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先ではStringの内部のオブジェクトをreleaseする
  • 呼び出し側では、いっさいreleaseしない
kabeyakabeya

8個目の調査で、letで宣言した変数を引数にしてイニシャライザを呼ぶ場合、呼び出し側でいっさいreleaseしないことが分かりました。

ということは、6個目の調査のまとめで「呼ぶ前にretainしているが、呼び出し後にその分のreleaseはしない。イニシャライザで返ってきた時点の分のreleaseのみ」と書きましたが、これは間違っていますね。

イニシャライザで返ってきた分をreleaseしないんですね。6個目でreleaseしているのは、呼び出し前にretainした分のみ、ということになります。

イニシャライザを呼ぶ場合、letだろうがvarだろうが、とにかく呼び出し側から呼び出し先(=イニシャライザ)に所有権が移る動きになっています。

kabeyakabeya

ここまでの調査まとめ

引数の型 var/let イニシャライザ/一般 param modifier 調査No. 呼び出し元の
呼び出し前retain
呼び出し元の
終了前release
呼び出し元の
終了時release
呼び出し先の
release
struct
(Intのみ)
var 一般 なし 1 なし なし なし なし
struct
(Intのみ)
let 一般 なし 2 なし なし なし なし
struct
(Intのみ)
var イニシャライザ なし 3 なし なし なし なし
struct
(Intのみ)
let イニシャライザ なし - (なし) (なし) (なし) (なし)
class var 一般 なし 4 あり あり あり なし
class let 一般 なし 5 なし なし あり なし
class var イニシャライザ なし 6 あり あり なし あり
class let イニシャライザ なし - (なし) (なし) (なし) (あり)
struct
(Stringあり)
var 一般 なし 7 あり あり あり なし
struct
(Stringあり)
let 一般 なし - (なし) (なし) (あり) (なし)
struct
(Stringあり)
var イニシャライザ なし - (あり) (なし) (あり) (あり)
struct
(Stringあり)
let イニシャライザ なし 8 なし なし なし あり

括弧書きの部分は他からの類推になります。

kabeyakabeya

次の調査の前に

呼び出し元でreleaseがなくなり代わりに呼び出し先にreleaseが追加されるのは、consuming(消費)呼び出し規約と言います。
呼び出し先でretainreleaseもしないのは、borrowing(借用)呼び出し規約と言います。

Swiftは関数のタイプによって以下のように自動で規約を選んでいます。

  • イニシャライザ、プロパティセッタ:consuming
  • それ以外の関数:borrowing

パラメータ修飾子のconsumingborrowingは、使用する規約を開発者側で明示的に変更するためのものです。

次以降の調査では、これらのパラメータ修飾子を付けるとどう変わるか見ていきます。
classstruct(Stringあり)→struct(Intのみ)→struct(Intのみ、~Copyable)の順で見ます。

最初のほうで~Copyableは見ません、とか書いてましたが、やっぱり見たほうがいいですね。

kabeyakabeya

9個目の調査

  • class
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:consuming
class TestData {
    var value1: Int = 0
    var value2: Int = 0
    
    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

var globalValue: Int = 0

func testFunc(_ testData: consuming TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}

予想ではmainFunc()側のreleaseが1つ減って、testFunc()releaseが1つ追加されるはずです。
どうでしょうか。

SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s5Test98mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %9, %13, %12
005:   %1 = metatype $@thick TestData.Type             // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.__allocating_init(value1:value2:)
011:   %6 = function_ref @$s5Test98TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // users: %9, %11, %8
013:   strong_retain %7 : $TestData                    // id: %8
014:   store %7 to %0 : $*TestData                     // id: %9
015:   // function_ref testFunc(_:)
016:   %10 = function_ref @$s5Test98testFuncyyAA8TestDataCnF : $@convention(thin) (@owned TestData) -> () // user: %11
017:   %11 = apply %10(%7) : $@convention(thin) (@owned TestData) -> ()
018:   destroy_addr %0 : $*TestData                    // id: %12
019:   dealloc_stack %0 : $*TestData                   // id: %13
020:   %14 = tuple ()                                  // user: %15
021:   return %14 : $()                                // id: %15
022: } // end sil function '$s5Test98mainFuncyyF'

mainFunc()側はstrong_releaseが1つ減りました。

SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s5Test98testFuncyyAA8TestDataCnF : $@convention(thin) (@owned TestData) -> () {
003: // %0 "testData"                                  // user: %3
004: bb0(%0 : @noImplicitCopy @_eagerMove $TestData):
005:   %1 = global_addr @$s5Test911globalValueSivp : $*Int // user: %9
006:   %2 = alloc_stack [moveable_value_debuginfo] $TestData, var, name "testData" // users: %3, %4, %13, %14
007:   store %0 to %2 : $*TestData                     // id: %3
008:   %4 = begin_access [read] [static] %2 : $*TestData // users: %5, %8
009:   %5 = load %4 : $*TestData                       // users: %6, %7
010:   %6 = class_method %5 : $TestData, #TestData.value2!getter : (TestData) -> () -> Int, $@convention(method) (@guaranteed TestData) -> Int // user: %7
011:   %7 = apply %6(%5) : $@convention(method) (@guaranteed TestData) -> Int // user: %10
012:   end_access %4 : $*TestData                      // id: %8
013:   %9 = begin_access [modify] [dynamic] %1 : $*Int // users: %10, %11
014:   store %7 to %9 : $*Int                          // id: %10
015:   end_access %9 : $*Int                           // id: %11
016:   debug_value undef : $*@moveOnly TestData, var, name "testData" // id: %12
017:   destroy_addr %2 : $*TestData                    // id: %13
018:   dealloc_stack %2 : $*TestData                   // id: %14
019:   %15 = tuple ()                                  // user: %16
020:   return %15 : $()                                // id: %16
021: } // end sil function '$s5Test98testFuncyyAA8TestDataCnF'

パラメータ修飾子なしのときはいきなり値にアクセスしていたのが、consumingを付けたらスタックに変数を確保して引数からコピーしています。
しかも、コピーしているにも関わらず、being_access/end_accessでガードするようになりました。
さらにdestroy_addr(=release)も追加されています。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 16], 0
007:         xor     eax, eax
008:         mov     edi, eax
009:         call    (type metadata accessor for output.TestData)
010:         mov     r13, rax
011:         mov     edi, 3
012:         mov     esi, 4
013:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
014:         mov     rdi, rax
015:         mov     qword ptr [rbp - 24], rdi
016:         call    swift_retain@PLT
017:         mov     rdi, qword ptr [rbp - 24]
018:         mov     qword ptr [rbp - 16], rdi
019:         call    (output.testFunc(__owned output.TestData) -> ())
020:         mov     rdi, qword ptr [rbp - 16]
021:         call    swift_release@PLT
022:         add     rsp, 24
023:         pop     r13
024:         pop     rbp
025:         ret

releaseが1つ減っている以外は特に大きな特徴はありません。

アセンブラ-testFunc()
001: output.testFunc(__owned output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 56
006:         mov     qword ptr [rbp - 48], rdi
007:         lea     rdi, [rbp - 16]
008:         xor     esi, esi
009:         mov     edx, 8
010:         call    memset@PLT
011:         mov     rax, qword ptr [rbp - 48]
012:         mov     qword ptr [rbp - 16], rax
013:         mov     r13, qword ptr [rbp - 16]
014:         mov     rax, qword ptr [r13]
015:         mov     rax, qword ptr [rax + 96]
016:         call    rax
017:         mov     qword ptr [rbp - 64], rax
018:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
019:         xor     eax, eax
020:         mov     ecx, eax
021:         lea     rsi, [rbp - 40]
022:         mov     qword ptr [rbp - 56], rsi
023:         mov     edx, 33
024:         call    swift_beginAccess@PLT
025:         mov     rax, qword ptr [rbp - 64]
026:         mov     rdi, qword ptr [rbp - 56]
027:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
028:         call    swift_endAccess@PLT
029:         mov     rdi, qword ptr [rbp - 16]
030:         call    swift_release@PLT
031:         add     rsp, 56
032:         pop     r13
033:         pop     rbp
034:         ret

SILではパラメータ修飾子なしのときとconsumingを付けたときで以下の部分が違っていましたが、アセンブラでは(若干回りくどさが違うものの)ほぼ同じになりました。

  • consumingを付けたら、引数を明示的にスタックの変数へコピーするようになった→アセンブラ上はもともとコピーしていたので差がない
  • consumingを付けたら、コピー後のスタックの変数にbeing_access/end_accessでアクセスをガードするようになった→アセンブラ上はbegin_access/end_accessがなくなった

このため、変化点としては「releaseが追加されたこと」のみとなっています。

まとめ

今回は以下のケースで調査しました。

  • class
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:consuming

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 渡す前に引数のオブジェクトをretainする
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では引数のオブジェクトをreleaseする
  • 呼び出し側では、最後、引数に使ったオブジェクトをreleaseしない
kabeyakabeya

10個目の調査

  • class
  • var
  • 「イニシャライザ 、プロパティセッタ」以外の関数
  • パラメータ修飾子:borrowing
class TestData {
    var value1: Int = 0
    var value2: Int = 0
    
    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

struct Foo {
    var propValue: Int = 0
    
    init(_ testData: borrowing TestData) {
        self.propValue = testData.value2
    }
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    var foo = Foo(testData)
    foo.propValue = 5
}

予想ではmainFunc()側のreleaseが1つ増えて、Foo.init(_:)からreleaseがなくなるはずです。

SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s6Test108mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %9, %24, %23
005:   %1 = metatype $@thick TestData.Type             // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.__allocating_init(value1:value2:)
011:   %6 = function_ref @$s6Test108TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // users: %14, %9, %13, %8
013:   strong_retain %7 : $TestData                    // id: %8
014:   store %7 to %0 : $*TestData                     // id: %9
015:   %10 = alloc_stack $Foo, var, name "foo"         // users: %15, %22, %18
016:   %11 = metatype $@thin Foo.Type                  // user: %13
017:   // function_ref Foo.init(_:)
018:   %12 = function_ref @$s6Test103FooVyAcA8TestDataChcfC : $@convention(method) (@guaranteed TestData, @thin Foo.Type) -> Foo // user: %13
019:   %13 = apply %12(%7, %11) : $@convention(method) (@guaranteed TestData, @thin Foo.Type) -> Foo // user: %15
020:   strong_release %7 : $TestData                   // id: %14
021:   store %13 to %10 : $*Foo                        // id: %15
022:   %16 = integer_literal $Builtin.Int64, 5         // user: %17
023:   %17 = struct $Int (%16 : $Builtin.Int64)        // user: %20
024:   %18 = begin_access [modify] [static] %10 : $*Foo // users: %21, %19
025:   %19 = struct_element_addr %18 : $*Foo, #Foo.propValue // user: %20
026:   store %17 to %19 : $*Int                        // id: %20
027:   end_access %18 : $*Foo                          // id: %21
028:   dealloc_stack %10 : $*Foo                       // id: %22
029:   destroy_addr %0 : $*TestData                    // id: %23
030:   dealloc_stack %0 : $*TestData                   // id: %24
031:   %25 = tuple ()                                  // user: %26
032:   return %25 : $()                                // id: %26
033: } // end sil function '$s6Test108mainFuncyyF'

予想通り、020にstrong_releaseが増えています。

SIL-Foo.init()
001: // Foo.init(_:)
002: sil hidden @$s6Test103FooVyAcA8TestDataChcfC : $@convention(method) (@guaranteed TestData, @thin Foo.Type) -> Foo {
003: // %0 "testData"                                  // users: %8, %9, %3
004: // %1 "$metatype"
005: bb0(%0 : @noImplicitCopy $TestData, %1 : $@thin Foo.Type):
006:   %2 = alloc_stack $Foo, var, name "self", implicit // users: %7, %10, %15
007:   debug_value [moveable_value_debuginfo] %0 : $TestData, let, name "testData", argno 1 // id: %3
008:   %4 = integer_literal $Builtin.Int64, 0          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
010:   %6 = struct $Foo (%5 : $Int)                    // user: %7
011:   store %6 to %2 : $*Foo                          // id: %7
012:   %8 = class_method %0 : $TestData, #TestData.value2!getter : (TestData) -> () -> Int, $@convention(method) (@guaranteed TestData) -> Int // user: %9
013:   %9 = apply %8(%0) : $@convention(method) (@guaranteed TestData) -> Int // users: %12, %14
014:   %10 = begin_access [modify] [static] %2 : $*Foo // users: %13, %11
015:   %11 = struct_element_addr %10 : $*Foo, #Foo.propValue // user: %12
016:   store %9 to %11 : $*Int                         // id: %12
017:   end_access %10 : $*Foo                          // id: %13
018:   %14 = struct $Foo (%9 : $Int)                   // user: %16
019:   dealloc_stack %2 : $*Foo                        // id: %15
020:   return %14 : $Foo                               // id: %16
021: } // end sil function '$s6Test103FooVyAcA8TestDataChcfC'

こちらも予想通り、019のdealloc_stackの前にあったstrong_releaseがなくなっています。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 40
006:         mov     qword ptr [rbp - 16], 0
007:         mov     qword ptr [rbp - 24], 0
008:         xor     eax, eax
009:         mov     edi, eax
010:         call    (type metadata accessor for output.TestData)
011:         mov     r13, rax
012:         mov     edi, 3
013:         mov     esi, 4
014:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
015:         mov     rdi, rax
016:         mov     qword ptr [rbp - 40], rdi
017:         call    swift_retain@PLT
018:         mov     rdi, qword ptr [rbp - 40]
019:         mov     qword ptr [rbp - 16], rdi
020:         call    (output.Foo.init(__shared output.TestData) -> output.Foo)
021:         mov     rdi, qword ptr [rbp - 40]
022:         mov     qword ptr [rbp - 32], rax
023:         call    swift_release@PLT
024:         mov     rax, qword ptr [rbp - 32]
025:         mov     qword ptr [rbp - 24], rax
026:         mov     qword ptr [rbp - 24], 5
027:         mov     rdi, qword ptr [rbp - 16]
028:         call    swift_release@PLT
029:         add     rsp, 40
030:         pop     r13
031:         pop     rbp
032:         ret

予想通りswift_releaseが1つ増えて2個あります。

アセンブラ-Foo.init()
001: output.Foo.init(__shared output.TestData) -> output.Foo:
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 32], rdi
007:         lea     rdi, [rbp - 16]
008:         xor     esi, esi
009:         mov     edx, 8
010:         call    memset@PLT
011:         lea     rdi, [rbp - 24]
012:         xor     esi, esi
013:         mov     edx, 8
014:         call    memset@PLT
015:         mov     rdi, qword ptr [rbp - 32]
016:         mov     qword ptr [rbp - 24], rdi
017:         mov     r13, qword ptr [rbp - 32]
018:         mov     qword ptr [rbp - 16], 0
019:         mov     rax, qword ptr [r13]
020:         mov     rax, qword ptr [rax + 96]
021:         call    rax
022:         mov     qword ptr [rbp - 16], rax
023:         add     rsp, 24
024:         pop     r13
025:         pop     rbp
026:         ret

当然、こちらにもswift_releaseはありません。

まとめ

今回は以下のケースで調査しました。

  • class
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:borrowing

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 渡す前に引数のオブジェクトをretainする
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では引数のオブジェクトをreleaseしない
  • 呼び出し側では、最後、引数に使ったオブジェクトをreleaseする
kabeyakabeya

class→struct(Stringあり)→struct(Intのみ)→struct(Intのみ、~Copyable)の順で見ます。

と書きましたが、structStringありはおそらくclassと同じ話にしかならないと思いますし、structIntのみはもともと一般関数とイニシャライザで違いがないので、おそらくconsumingborrowingつけても違いがないので、structIntのみかつ~Copyableに行きたいと思います。

11個目の調査

~Copyableというのは、コピーできない型を示すプロトコルです。
コピーできる型は暗黙的にCopyableプロトコルに沿い、コピーできない型として~Copyableがあります。

~CopyableはSwift 5.10時点でstructまたはenumだけに使えます。

いくつか機能がありますが、今回の調査では以下の機能に着目します。

  • 関数の引数にする際、呼び出し先にborrowingまたはconsumingのいずれかが付いていないと呼べない
  • structだけれどもdeinitが呼ばれる

結局のところ、classと同様、以下のようになるのだろうと推測しますが、実際そうなのかを確認します。

  • borrowingの場合:呼び出し元でdestroy_addrが呼ばれる(deinitされる)
  • consumingの場合:呼び出し先でdestroy_addrが呼ばれる(deinitされる)

まずはborrowingから行きます。

struct TestData: ~Copyable {
    var value1: Int = 0
    var value2: Int = 0

    deinit {
        print("deinit");
    }
}

var globalValue: Int = 0

func testFunc(_ testData: borrowing TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}
SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s6Test118mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %8, %9, %15, %16
005:   %1 = metatype $@thin TestData.Type              // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.init(value1:value2:)
011:   %6 = function_ref @$s6Test118TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> @owned TestData // user: %8
013:   store %7 to %0 : $*TestData                     // id: %8
014:   %9 = begin_access [read] [static] %0 : $*TestData // users: %10, %13
015:   %10 = load %9 : $*TestData                      // user: %12
016:   // function_ref testFunc(_:)
017:   %11 = function_ref @$s6Test118testFuncyyAA8TestDataVF : $@convention(thin) (@guaranteed TestData) -> () // user: %12
018:   %12 = apply %11(%10) : $@convention(thin) (@guaranteed TestData) -> ()
019:   end_access %9 : $*TestData                      // id: %13
020:   debug_value undef : $*TestData, var, name "testData" // id: %14
021:   destroy_addr %0 : $*TestData                    // id: %15
022:   dealloc_stack %0 : $*TestData                   // id: %16
023:   %17 = tuple ()                                  // user: %18
024:   return %17 : $()                                // id: %18
025: } // end sil function '$s6Test118mainFuncyyF'

予想通りdestroy_addrが呼ばれています。

SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s6Test118testFuncyyAA8TestDataVF : $@convention(thin) (@guaranteed TestData) -> () {
003: // %0 "testData"                                  // users: %3, %2
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s6Test1111globalValueSivp : $*Int // user: %4
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   %3 = struct_extract %0 : $TestData, #TestData.value2 // user: %5
008:   %4 = begin_access [modify] [dynamic] %1 : $*Int // users: %5, %6
009:   store %3 to %4 : $*Int                          // id: %5
010:   end_access %4 : $*Int                           // id: %6
011:   %7 = tuple ()                                   // user: %8
012:   return %7 : $()                                 // id: %8
013: } // end sil function '$s6Test118testFuncyyAA8TestDataVF'

こちらはdestroy_addrが呼ばれていません。これも予想通りです。
引数に@guaranteedがついている部分以外は1番目の調査と同じです。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         lea     rdi, [rbp - 16]
006:         xor     esi, esi
007:         mov     edx, 16
008:         call    memset@PLT
009:         mov     edi, 3
010:         mov     esi, 4
011:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
012:         mov     qword ptr [rbp - 16], rax
013:         mov     qword ptr [rbp - 8], rdx
014:         mov     rdi, qword ptr [rbp - 16]
015:         mov     rsi, qword ptr [rbp - 8]
016:         call    (output.testFunc(output.TestData) -> ())
017:         mov     rdi, qword ptr [rbp - 16]
018:         mov     rsi, qword ptr [rbp - 8]
019:         call    (output.TestData.deinit)
020:         add     rsp, 16
021:         pop     rbp
022:         ret

SILのdestroy_addrは、アセンブラではdeinitに置き換わっています。
deinitの呼び出しがある以外は1番目の調査とほぼ同じです[1]

アセンブラ-testFunc()
001: output.testFunc(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 64
005:         mov     qword ptr [rbp - 48], rsi
006:         mov     qword ptr [rbp - 56], rdi
007:         lea     rdi, [rbp - 16]
008:         xor     esi, esi
009:         mov     edx, 16
010:         call    memset@PLT
011:         mov     rdi, qword ptr [rbp - 56]
012:         mov     rsi, qword ptr [rbp - 48]
013:         mov     qword ptr [rbp - 16], rdi
014:         mov     qword ptr [rbp - 8], rsi
015:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
016:         xor     eax, eax
017:         mov     ecx, eax
018:         lea     rsi, [rbp - 40]
019:         mov     qword ptr [rbp - 64], rsi
020:         mov     edx, 33
021:         call    swift_beginAccess@PLT
022:         mov     rax, qword ptr [rbp - 48]
023:         mov     rdi, qword ptr [rbp - 64]
024:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
025:         call    swift_endAccess@PLT
026:         add     rsp, 64
027:         pop     rbp
028:         ret

こちらもローカル変数の順序が若干変わっている以外は、1番目の調査とほぼ同じです[2]

まとめ

今回は以下のケースで調査しました。

  • struct(Intのみ、~Copyable)
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:borrowing

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では引数のオブジェクトをdestroy_addrしない
  • 呼び出し側では、最後、引数に使ったオブジェクトをdestroy_addrする

また、「~Copyableはコピーできない」とは言っても、実際に本当にレジスタやメモリでコピーされないということではありません。
値による引数渡しはそのままで、参照による引数渡しになったりはしません。

一方で、手動でコピーする場合(つまり引数以外の変数にコピーする場合)は、エラーになります。

struct TestData: ~Copyable {
    var value1: Int = 0
    var value2: Int = 0

    deinit {
        print("deinit");
    }
}

var globalValue: TestData = TestData()

func testFunc(_ testData: borrowing TestData) {
    globalValue = testData  // ←これはエラーになる
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}

自分はこの調査するまで、参照渡しになるのかなと思っていたんですが、そうではないということが分かりました[3]

脚注
  1. 1番目の調査はxmm0レジスタを使って0クリアしていましたが、こちらはmemsetを使っています ↩︎

  2. こちらもxmm0レジスタ→memsetになっています ↩︎

  3. 11個目まで来る間に理解が深まっていたので、11個目時点ではそうじゃなさそうということは予想できましたが、確認できてよかったです。 ↩︎

kabeyakabeya

12個目の調査

ついでconsumingに行きます。

struct TestData: ~Copyable {
    var value1: Int = 0
    var value2: Int = 0

    deinit {
        print("deinit");
    }
}

var globalValue: Int = 0

func testFunc(_ testData: consuming TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}

呼び出し元ではdestroy_addrされず、呼び出し先でdestroy_addrが呼ばれる(deinitされる)だろうと予想されます。
もちろん、これも値による引数渡しのままで、参照による引数渡しになったりはしないはずです。

SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s6Test128mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %8, %9, %15
005:   %1 = metatype $@thin TestData.Type              // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.init(value1:value2:)
011:   %6 = function_ref @$s6Test128TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> @owned TestData // user: %8
013:   store %7 to %0 : $*TestData                     // id: %8
014:   %9 = begin_access [deinit] [static] %0 : $*TestData // users: %10, %14
015:   %10 = load %9 : $*TestData                      // user: %13
016:   debug_value undef : $*TestData, var, name "testData" // id: %11
017:   // function_ref testFunc(_:)
018:   %12 = function_ref @$s6Test128testFuncyyAA8TestDataVnF : $@convention(thin) (@owned TestData) -> () // user: %13
019:   %13 = apply %12(%10) : $@convention(thin) (@owned TestData) -> ()
020:   end_access %9 : $*TestData                      // id: %14
021:   dealloc_stack %0 : $*TestData                   // id: %15
022:   %16 = tuple ()                                  // user: %17
023:   return %16 : $()                                // id: %17
024: } // end sil function '$s6Test128mainFuncyyF'

destroy_addrがなくなりました。

SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s6Test128testFuncyyAA8TestDataVnF : $@convention(thin) (@owned TestData) -> () {
003: // %0 "testData"                                  // user: %3
004: bb0(%0 : $TestData):
005:   %1 = global_addr @$s6Test1211globalValueSivp : $*Int // user: %8
006:   %2 = alloc_stack [lexical] $TestData, var, name "testData" // users: %3, %4, %12, %13
007:   store %0 to %2 : $*TestData                     // id: %3
008:   %4 = begin_access [read] [static] %2 : $*TestData // users: %5, %7
009:   %5 = struct_element_addr %4 : $*TestData, #TestData.value2 // user: %6
010:   %6 = load %5 : $*Int                            // user: %9
011:   end_access %4 : $*TestData                      // id: %7
012:   %8 = begin_access [modify] [dynamic] %1 : $*Int // users: %9, %10
013:   store %6 to %8 : $*Int                          // id: %9
014:   end_access %8 : $*Int                           // id: %10
015:   debug_value undef : $*TestData, var, name "testData" // id: %11
016:   destroy_addr %2 : $*TestData                    // id: %12
017:   dealloc_stack %2 : $*TestData                   // id: %13
018:   %14 = tuple ()                                  // user: %15
019:   return %14 : $()                                // id: %15
020: } // end sil function '$s6Test128testFuncyyAA8TestDataVnF'

こちらにdestroy_addrが来ました。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         lea     rdi, [rbp - 16]
006:         xor     esi, esi
007:         mov     edx, 16
008:         call    memset@PLT
009:         mov     edi, 3
010:         mov     esi, 4
011:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
012:         mov     qword ptr [rbp - 16], rax
013:         mov     qword ptr [rbp - 8], rdx
014:         mov     rdi, qword ptr [rbp - 16]
015:         mov     rsi, qword ptr [rbp - 8]
016:         call    (output.testFunc(__owned output.TestData) -> ())
017:         add     rsp, 16
018:         pop     rbp
019:         ret

deinitがなくなりました。

アセンブラ-testFunc()
001: output.testFunc(__owned output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 80
005:         mov     qword ptr [rbp - 56], rsi
006:         mov     qword ptr [rbp - 48], rdi
007:         lea     rdi, [rbp - 16]
008:         xor     esi, esi
009:         mov     edx, 16
010:         call    memset@PLT
011:         mov     rax, qword ptr [rbp - 56]
012:         mov     rcx, qword ptr [rbp - 48]
013:         mov     qword ptr [rbp - 16], rcx
014:         mov     qword ptr [rbp - 8], rax
015:         mov     rax, qword ptr [rbp - 8]
016:         mov     qword ptr [rbp - 72], rax
017:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
018:         xor     eax, eax
019:         mov     ecx, eax
020:         lea     rsi, [rbp - 40]
021:         mov     qword ptr [rbp - 64], rsi
022:         mov     edx, 33
023:         call    swift_beginAccess@PLT
024:         mov     rax, qword ptr [rbp - 72]
025:         mov     rdi, qword ptr [rbp - 64]
026:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
027:         call    swift_endAccess@PLT
028:         mov     rdi, qword ptr [rbp - 16]
029:         mov     rsi, qword ptr [rbp - 8]
030:         call    (output.TestData.deinit)
031:         add     rsp, 80
032:         pop     rbp
033:         ret

deinitの呼び出しが増えています。それに伴ってローカル変数も2つ増えていますが、差はそれぐらいです。

まとめ

今回は以下のケースで調査しました。

  • struct(Intのみ、~Copyable)
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:consuming

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
  • 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
  • 呼び出し先では引数のオブジェクトをdestroy_addrする
  • 呼び出し元では、引数に使ったオブジェクトをdestroy_addrしない

ちなみに手動でコピーする場合(つまり引数以外の変数にコピーする場合)は、1回目はエラーになりませんが、2回目以降はエラーになります。

struct TestData: ~Copyable {
    var value1: Int = 0
    var value2: Int = 0

    deinit {
        print("deinit");
    }
}

var globalValue: TestData = TestData()

func testFunc(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
    globalValue = testData  // ←これはエラーになる
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(testData)
}

また2行目の代入がなくコンパイルできた場合、testFunc()では旧globalValuedeinitが呼ばれ、testDatadeinitは呼ばれません。

kabeyakabeya

13個目の調査

inoutの調査をします。

  • struct(Intのみ)
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:inout
struct TestData {
    var value1: Int = 0
    var value2: Int = 0
}

var globalValue: Int = 0

func testFunc(_ testData: inout TestData) {
    globalValue = testData.value2
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
	testFunc(&testData)
}

値による引数渡しから、参照による引数渡しになるはずです。
SILやアセンブラがどうなるか楽しみです。

SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s6Test138mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack $TestData, var, name "testData" // users: %8, %13, %9
005:   %1 = metatype $@thin TestData.Type              // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.init(value1:value2:)
011:   %6 = function_ref @$s6Test138TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %8
013:   store %7 to %0 : $*TestData                     // id: %8
014:   %9 = begin_access [modify] [static] %0 : $*TestData // users: %12, %11
015:   // function_ref testFunc(_:)
016:   %10 = function_ref @$s6Test138testFuncyyAA8TestDataVzF : $@convention(thin) (@inout TestData) -> () // user: %11
017:   %11 = apply %10(%9) : $@convention(thin) (@inout TestData) -> ()
018:   end_access %9 : $*TestData                      // id: %12
019:   dealloc_stack %0 : $*TestData                   // id: %13
020:   %14 = tuple ()                                  // user: %15
021:   return %14 : $()                                // id: %15
022: } // end sil function '$s6Test138mainFuncyyF'

SIL上は、単に関数の引数に@inoutが付くだけで、アドレスを取得する処理が明示的に出力されるわけではないんですね。
begin_access/end_accessも入っています。ただし、このbegin_access/end_accessは、ここまでの経験から推測するに、アセンブラになったときには消えてなくなりそうです。

SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s6Test138testFuncyyAA8TestDataVzF : $@convention(thin) (@inout TestData) -> () {
003: // %0 "testData"                                  // users: %3, %2
004: bb0(%0 : $*TestData):
005:   %1 = global_addr @$s6Test1311globalValueSivp : $*Int // user: %7
006:   debug_value %0 : $*TestData, var, name "testData", argno 1, expr op_deref // id: %2
007:   %3 = begin_access [read] [static] %0 : $*TestData // users: %6, %4
008:   %4 = struct_element_addr %3 : $*TestData, #TestData.value2 // user: %5
009:   %5 = load %4 : $*Int                            // user: %8
010:   end_access %3 : $*TestData                      // id: %6
011:   %7 = begin_access [modify] [dynamic] %1 : $*Int // users: %8, %9
012:   store %5 to %7 : $*Int                          // id: %8
013:   end_access %7 : $*Int                           // id: %9
014:   %10 = tuple ()                                  // user: %11
015:   return %10 : $()                                // id: %11
016: } // end sil function '$s6Test138testFuncyyAA8TestDataVzF'

004行で受け取る型が、$TestDataではなく$*TestDataに変わっています。
他にもbegin_access/end_accessが入ったり、struct_extractstruct_element_extractに変わったりと、いくつか変わっていますね。
このbegin_access/end_access[read][static]のほう)もアセンブラでは消えるでしょう。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 16
005:         xorps   xmm0, xmm0
006:         movaps  xmmword ptr [rbp - 16], xmm0
007:         mov     edi, 3
008:         mov     esi, 4
009:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
010:         mov     qword ptr [rbp - 16], rax
011:         mov     qword ptr [rbp - 8], rdx
012:         lea     rdi, [rbp - 16]
013:         call    (output.testFunc(inout output.TestData) -> ())
014:         add     rsp, 16
015:         pop     rbp
016:         ret

SILではアドレスを取得するような記述はなかったのですが、さすがにアセンブラでは012でアドレスを取得しています。
そしてbegin_access/end_accessはやはり消えました。

アセンブラ-testFunc()
001: output.testFunc(inout output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 48
005:         mov     qword ptr [rbp - 8], 0
006:         mov     qword ptr [rbp - 8], rdi
007:         mov     rax, qword ptr [rdi + 8]
008:         mov     qword ptr [rbp - 48], rax
009:         lea     rdi, [rip + (output.globalValue : Swift.Int)]
010:         xor     eax, eax
011:         mov     ecx, eax
012:         lea     rsi, [rbp - 32]
013:         mov     qword ptr [rbp - 40], rsi
014:         mov     edx, 33
015:         call    swift_beginAccess@PLT
016:         mov     rax, qword ptr [rbp - 48]
017:         mov     rdi, qword ptr [rbp - 40]
018:         mov     qword ptr [rip + (output.globalValue : Swift.Int)], rax
019:         call    swift_endAccess@PLT
020:         add     rsp, 48
021:         pop     rbp
022:         ret

007で引数のポインタ(rdi)から8バイトオフセットしたところの値、つまりvalue2を取っています。
こういう処理はすごくシンプルに変換されるんですね。

まとめ

今回は以下のケースで調査しました。

  • struct(Intのみ)
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:inout

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元で変数のアドレスがレジスタにコピーされて、呼び出し先に渡る
  • 呼び出し先ではレジスタにあるポインタを使って直接アクセスする。ただしポインタはローカル変数にもコピーする
kabeyakabeya

14個目の調査

inoutの調査をclassでします。もともと参照型なので参照(ポインタ)が渡っているんですが、inoutで渡して呼び出し先でその参照を書き換えたらどういう処理になるのかを確認します。

  • class
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:inout
class TestData {
    var value1: Int = 0
    var value2: Int = 0

    init(value1: Int, value2: Int) {
        self.value1 = value1
        self.value2 = value2
    }
}

func testFunc(_ testData: inout TestData) {
    testData = TestData(value1: 5, value2: 6)
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc(&testData)
}
SIL-mainFunc()
001: // mainFunc()
002: sil hidden @$s6Test148mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack [lexical] $TestData, var, name "testData" // users: %8, %14, %13, %9
005:   %1 = metatype $@thick TestData.Type             // user: %7
006:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
007:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
008:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
009:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
010:   // function_ref TestData.__allocating_init(value1:value2:)
011:   %6 = function_ref @$s6Test148TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %7
012:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %8
013:   store %7 to %0 : $*TestData                     // id: %8
014:   %9 = begin_access [modify] [static] %0 : $*TestData // users: %12, %11
015:   // function_ref testFunc(_:)
016:   %10 = function_ref @$s6Test148testFuncyyAA8TestDataCzF : $@convention(thin) (@inout TestData) -> () // user: %11
017:   %11 = apply %10(%9) : $@convention(thin) (@inout TestData) -> ()
018:   end_access %9 : $*TestData                      // id: %12
019:   destroy_addr %0 : $*TestData                    // id: %13
020:   dealloc_stack %0 : $*TestData                   // id: %14
021:   %15 = tuple ()                                  // user: %16
022:   return %15 : $()                                // id: %16
023: } // end sil function '$s6Test148mainFuncyyF'

4個目の調査と比較すると、strong_retain/strong_releaseがなくなっていることが分かります。
@borrowingでも@consumingでも、varの場合はstrong_retainがあったのですが、@inoutにするとletのようにstrong_retainがなくなるということなんですね。

またポインタを取得する操作的なものはやはりありません。

SIL-testFunc()
001: // testFunc(_:)
002: sil hidden @$s6Test148testFuncyyAA8TestDataCzF : $@convention(thin) (@inout TestData) -> () {
003: // %0 "testData"                                  // users: %9, %1
004: bb0(%0 : $*TestData):
005:   debug_value %0 : $*TestData, var, name "testData", argno 1, expr op_deref // id: %1
006:   %2 = metatype $@thick TestData.Type             // user: %8
007:   %3 = integer_literal $Builtin.Int64, 5          // user: %4
008:   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %8
009:   %5 = integer_literal $Builtin.Int64, 6          // user: %6
010:   %6 = struct $Int (%5 : $Builtin.Int64)          // user: %8
011:   // function_ref TestData.__allocating_init(value1:value2:)
012:   %7 = function_ref @$s6Test148TestDataC6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %8
013:   %8 = apply %7(%4, %6, %2) : $@convention(method) (Int, Int, @thick TestData.Type) -> @owned TestData // user: %11
014:   %9 = begin_access [modify] [static] %0 : $*TestData // users: %11, %10, %13
015:   %10 = load %9 : $*TestData                      // user: %12
016:   store %8 to %9 : $*TestData                     // id: %11
017:   strong_release %10 : $TestData                  // id: %12
018:   end_access %9 : $*TestData                      // id: %13
019:   %14 = tuple ()                                  // user: %15
020:   return %14 : $()                                // id: %15
021: } // end sil function '$s6Test148testFuncyyAA8TestDataCzF'

016で引数の「参照のポインタ」に新たに生成したTestDataオブジェクトの参照を設定しています。
017で引数で受け取った、元のオブジェクトの参照に対してstrong_releaseしています。

改めて書くと普通ですね。

アセンブラ-mainFunc()
001: output.mainFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         push    rax
006:         mov     qword ptr [rbp - 16], 0
007:         xor     eax, eax
008:         mov     edi, eax
009:         call    (type metadata accessor for output.TestData)
010:         mov     r13, rax
011:         mov     edi, 3
012:         mov     esi, 4
013:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
014:         mov     qword ptr [rbp - 16], rax
015:         lea     rdi, [rbp - 16]
016:         call    (output.testFunc(inout output.TestData) -> ())
017:         mov     rdi, qword ptr [rbp - 16]
018:         call    swift_release@PLT
019:         add     rsp, 8
020:         pop     r13
021:         pop     rbp
022:         ret

流れは以下のようになっています。

  • 015でTestDataの参照の入ってるメモリ領域のポインタをrdiに入れる
  • 016でtestFunc()を呼ぶ(第1引数=rdi)
  • 017でそのメモリ領域からrdiに参照を取り出す
  • 018でswift_releaseする(第1引数=rdi)

シンプルですね。testFunc()で参照を書き換えてようが書き換えていまいが、017では取り出し直すのだろうと推測します[1]

アセンブラ-testFunc()
001: output.testFunc(inout output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 24
006:         mov     qword ptr [rbp - 24], rdi
007:         mov     qword ptr [rbp - 16], 0
008:         mov     qword ptr [rbp - 16], rdi
009:         xor     eax, eax
010:         mov     edi, eax
011:         call    (type metadata accessor for output.TestData)
012:         mov     r13, rax
013:         mov     edi, 5
014:         mov     esi, 6
015:         call    (output.TestData.__allocating_init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
016:         mov     rcx, rax
017:         mov     rax, qword ptr [rbp - 24]
018:         mov     rdi, qword ptr [rax]
019:         mov     qword ptr [rax], rcx
020:         call    swift_release@PLT
021:         add     rsp, 24
022:         pop     r13
023:         pop     rbp
024:         ret
  • 006で引数(rdi)にある参照のポインタを[rbp-24]に入れる
  • 017でraxに[rbp-24]の値(引数で受け取った、参照のポインタ)を入れる
  • 018でrdiに[rax]の値(引数で受け取った、参照のポインタの指す値=参照=旧TestData)を入れる
  • 019で[rax](引数で受け取った、参照のポインタ)に新たに生成したTestDataの参照を入れる
  • 020でrdi(旧TestData)をswift_releaseする

こちらもシンプルです。

まとめ

今回は以下のケースで調査しました。

  • class
  • var
  • 「イニシャライザ、プロパティセッタ」以外の関数
  • パラメータ修飾子:inout

SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。

  • 引数は呼び出し元で変数のアドレスがレジスタにコピーされて、呼び出し先に渡る
  • 呼び出し先ではレジスタにあるポインタをローカル変数にコピーする
  • 呼び出し元では、inoutの引数で渡した参照のポインタから参照をもう1回取り出し直す

最後、取り出し直しているのはreleaseするためですね。
13個目の調査でstructを取り出し直さなかったのは、そのあと渡した変数に触っておらず、releaseする必要もなかったから、と推測します[2]

脚注
  1. 確認しましたが、取り出し直しますね ↩︎

  2. 呼び出し後に触ると取り出し直すことを確認しました ↩︎

kabeyakabeya

15個目の調査(脱線)

ちょっと脱線なんですけれども、borrowingconsumingの代入・コピーの制約を調査します。
Swiftだけで確認します。

No. パラメータ修飾子 Copyable/~Copyable
15_1 borrowing Copyable
15_2 borrowing ~Copyable
15_3 consuming Copyable
15_4 consuming ~Copyable
Test15_1.swift
struct TestData {
    var value1: Int = 0
    var value2: Int = 0
}

var globalValue: TestData = TestData()

func testFunc1(_ testData: borrowing TestData) {
    globalValue = testData  // ←これはエラーになる
}

func testFunc2(_ testData: borrowing TestData) {
    globalValue = copy testData  // ←これはエラーにならない
}

func testFunc3(_ testData: borrowing TestData) {
    globalValue = copy testData  // ←これはエラーにならない
    globalValue = copy testData  // ←これはエラーにならない
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc1(testData)
    testFunc2(testData)
    testFunc3(testData)
    testData.value1 = 5
}
Test15_2.swift
struct TestData : ~Copyable {
    var value1: Int = 0
    var value2: Int = 0
}

var globalValue: TestData = TestData()

func testFunc1(_ testData: borrowing TestData) {
    globalValue = testData  // ←これはエラーになる
}

func testFunc2(_ testData: borrowing TestData) {
    globalValue = copy testData  // ←これもエラーになる
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc1(testData)
    testFunc2(testData)
    testData.value1 = 5
}
Test15_3.swift
struct TestData {
    var value1: Int = 0
    var value2: Int = 0
}

var globalValue: TestData = TestData()

func testFunc1(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
}

func testFunc2(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
    globalValue = testData  // ←これはエラーになる
}

func testFunc3(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
    globalValue = copy testData  // ←これはエラーになる
}

func testFunc4(_ testData: consuming TestData) {
    globalValue = copy testData  // ←これはエラーにならない
    globalValue = testData  // ←これはエラーにならない
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc1(testData)
    testFunc2(testData) // ←これはエラーにならない
    testFunc3(testData) // ←これもエラーにならない
    testFunc4(testData) // ←これもエラーにならない
    testData.value1 = 5 // ←これもエラーにならない
}
Test15_4.swift
struct TestData : ~Copyable {
    var value1: Int = 0
    var value2: Int = 0
}

var globalValue: TestData = TestData()

func testFunc1(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
}

func testFunc2(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
    globalValue = testData  // ←これはエラーになる
}

func testFunc3(_ testData: consuming TestData) {
    globalValue = testData  // ←これはエラーにならない
    globalValue = copy testData  // ←これはエラーになる
}

func testFunc4(_ testData: consuming TestData) {
    globalValue = copy testData  // ←これはエラーになる
    globalValue = testData  // ←これはエラーにならない
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    testFunc1(testData)
    testFunc2(testData) // ←これはエラーになる
    testFunc3(testData) // ←これもエラーになる
    testFunc4(testData) // ←これもエラーになる
    testData.value1 = 5 // ←これもエラーになる
}

ちょっと驚いた点としては以下が挙げられます。

  • 15_3で、testFunc3()がダメでtestFunc4()はOKという点。代入の時点で消費されてしまっているので、それ以上のアクセスはもうできない、という制約が効いている
  • 15_3で、mainFunc()testFunc1()後にもtestDataをまだいじれるという点。consumingされているのに…
kabeyakabeya

もう少しよく調べると、イニシャライザ/プロパティセッタかそれ以外か、という話ではない気がしてきました。

Ownershipには、nonmutatingな関数のselfmutatingな関数のselfという書き方がしてあります。しかもmutatingなら参照による引数渡しになるというんですね。

このドキュメント自体は、Swiftの言語をどうしてくかというような方向性の話を語るものであって、現在および未来の仕様を定義するものではないのですが、実際現行の仕様がどうなっているのか、上記の観点で確認する必要がありますね。

kabeyakabeya

16個目の調査

mutating/nonmutatingに着目して調査します。

struct TestData {
    var value1: Int = 0
    var value2: Int = 0
}

struct Foo {
    var propValue: Int = 0
    
    init(_ testData: TestData) {
        self.propValue = testData.value2
    }

    func doSomething1(_ testData: TestData) {
        //self.propValue = testData.value1
    }
    mutating func doSomething2(_ testData: TestData) {
        self.propValue = testData.value1
    }
}

func mainFunc() {
    var testData = TestData(value1: 3, value2: 4)
    var foo = Foo(testData)
    foo.doSomething1(testData)
    foo.doSomething2(testData)
}
SIL
001: // Foo.doSomething1(_:)
002: sil hidden @$s6Test163FooV12doSomething1yyAA8TestDataVF : $@convention(method) (TestData, Foo) -> () {
003: // %0 "testData"                                  // user: %2
004: // %1 "self"                                      // user: %3
005: bb0(%0 : $TestData, %1 : $Foo):
006:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
007:   debug_value %1 : $Foo, let, name "self", argno 2, implicit // id: %3
008:   %4 = tuple ()                                   // user: %5
009:   return %4 : $()                                 // id: %5
010: } // end sil function '$s6Test163FooV12doSomething1yyAA8TestDataVF'
011: 
012: // Foo.doSomething2(_:)
013: sil hidden @$s6Test163FooV12doSomething2yyAA8TestDataVF : $@convention(method) (TestData, @inout Foo) -> () {
014: // %0 "testData"                                  // users: %4, %2
015: // %1 "self"                                      // users: %5, %3
016: bb0(%0 : $TestData, %1 : $*Foo):
017:   debug_value %0 : $TestData, let, name "testData", argno 1 // id: %2
018:   debug_value %1 : $*Foo, var, name "self", argno 2, implicit, expr op_deref // id: %3
019:   %4 = struct_extract %0 : $TestData, #TestData.value1 // user: %7
020:   %5 = begin_access [modify] [static] %1 : $*Foo  // users: %8, %6
021:   %6 = struct_element_addr %5 : $*Foo, #Foo.propValue // user: %7
022:   store %4 to %6 : $*Int                          // id: %7
023:   end_access %5 : $*Foo                           // id: %8
024:   %9 = tuple ()                                   // user: %10
025:   return %9 : $()                                 // id: %10
026: } // end sil function '$s6Test163FooV12doSomething2yyAA8TestDataVF'
027: 
028: // mainFunc()
029: sil hidden @$s6Test168mainFuncyyF : $@convention(thin) () -> () {
030: bb0:
031:   %0 = alloc_stack $TestData, var, name "testData" // users: %8, %21
032:   %1 = metatype $@thin TestData.Type              // user: %7
033:   %2 = integer_literal $Builtin.Int64, 3          // user: %3
034:   %3 = struct $Int (%2 : $Builtin.Int64)          // user: %7
035:   %4 = integer_literal $Builtin.Int64, 4          // user: %5
036:   %5 = struct $Int (%4 : $Builtin.Int64)          // user: %7
037:   // function_ref TestData.init(value1:value2:)
038:   %6 = function_ref @$s6Test168TestDataV6value16value2ACSi_SitcfC : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // user: %7
039:   %7 = apply %6(%3, %5, %1) : $@convention(method) (Int, Int, @thin TestData.Type) -> TestData // users: %8, %18, %15, %12
040:   store %7 to %0 : $*TestData                     // id: %8
041:   %9 = alloc_stack $Foo, var, name "foo"          // users: %13, %20, %16
042:   %10 = metatype $@thin Foo.Type                  // user: %12
043:   // function_ref Foo.init(_:)
044:   %11 = function_ref @$s6Test163FooVyAcA8TestDataVcfC : $@convention(method) (TestData, @thin Foo.Type) -> Foo // user: %12
045:   %12 = apply %11(%7, %10) : $@convention(method) (TestData, @thin Foo.Type) -> Foo // users: %13, %15
046:   store %12 to %9 : $*Foo                         // id: %13
047:   // function_ref Foo.doSomething1(_:)
048:   %14 = function_ref @$s6Test163FooV12doSomething1yyAA8TestDataVF : $@convention(method) (TestData, Foo) -> () // user: %15
049:   %15 = apply %14(%7, %12) : $@convention(method) (TestData, Foo) -> ()
050:   %16 = begin_access [modify] [static] %9 : $*Foo // users: %19, %18
051:   // function_ref Foo.doSomething2(_:)
052:   %17 = function_ref @$s6Test163FooV12doSomething2yyAA8TestDataVF : $@convention(method) (TestData, @inout Foo) -> () // user: %18
053:   %18 = apply %17(%7, %16) : $@convention(method) (TestData, @inout Foo) -> ()
054:   end_access %16 : $*Foo                          // id: %19
055:   dealloc_stack %9 : $*Foo                        // id: %20
056:   dealloc_stack %0 : $*TestData                   // id: %21
057:   %22 = tuple ()                                  // user: %23
058:   return %22 : $()                                // id: %23
059: } // end sil function '$s6Test168mainFuncyyF'

nonmutatingdoSomething1(_:)の隠し引数(self)はFooで、mutatingdoSomething2(:_)の隠し引数(self)は@inout Fooなんですね。

アセンブラ
001: output.Foo.doSomething1(output.TestData) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         xorps   xmm0, xmm0
005:         movaps  xmmword ptr [rbp - 16], xmm0
006:         mov     qword ptr [rbp - 24], 0
007:         mov     qword ptr [rbp - 16], rdi
008:         mov     qword ptr [rbp - 8], rsi
009:         mov     qword ptr [rbp - 24], rdx
010:         pop     rbp
011:         ret
012: 
013: output.Foo.doSomething2(output.TestData) -> ():
014:         push    rbp
015:         mov     rbp, rsp
016:         xorps   xmm0, xmm0
017:         movaps  xmmword ptr [rbp - 16], xmm0
018:         mov     qword ptr [rbp - 24], 0
019:         mov     qword ptr [rbp - 16], rdi
020:         mov     qword ptr [rbp - 8], rsi
021:         mov     qword ptr [rbp - 24], r13
022:         mov     qword ptr [r13], rdi
023:         pop     rbp
024:         ret
025: 
026: output.mainFunc() -> ():
027:         push    rbp
028:         mov     rbp, rsp
029:         push    r13
030:         sub     rsp, 56
031:         xorps   xmm0, xmm0
032:         movaps  xmmword ptr [rbp - 32], xmm0
033:         mov     qword ptr [rbp - 40], 0
034:         mov     edi, 3
035:         mov     esi, 4
036:         call    (output.TestData.init(value1: Swift.Int, value2: Swift.Int) -> output.TestData)
037:         mov     rdi, rax
038:         mov     qword ptr [rbp - 56], rdi
039:         mov     rsi, rdx
040:         mov     qword ptr [rbp - 48], rsi
041:         mov     qword ptr [rbp - 32], rdi
042:         mov     qword ptr [rbp - 24], rsi
043:         call    (output.Foo.init(output.TestData) -> output.Foo)
044:         mov     rdi, qword ptr [rbp - 56]
045:         mov     rsi, qword ptr [rbp - 48]
046:         mov     rdx, rax
047:         mov     qword ptr [rbp - 40], rdx
048:         call    (output.Foo.doSomething1(output.TestData) -> ())
049:         mov     rdi, qword ptr [rbp - 56]
050:         mov     rsi, qword ptr [rbp - 48]
051:         lea     r13, [rbp - 40]
052:         call    (output.Foo.doSomething2(output.TestData) -> ())
053:         add     rsp, 56
054:         pop     r13
055:         pop     rbp
056:         ret

doSomething1(_:)の隠し引数(self)は、実際にはselfではなくてselfのコピーです。doSomething2(:_)の隠し引数(self)はfooのアドレスで、まさにselfが渡っています。

というわけで、nonmutatingmutating云々というのは、selfの渡し方に関する話でした(そう書いてあったんですが、そういうことでした)。

kabeyakabeya

私は、Ownershipに書いてあるsharedが、最終的にborrowingになったのかと思って色々と調べ始めました。

上記ドキュメントの書き方だと、inoutがC言語でいうところのT*を作るのに対して、sharedconst T*を作る、というふうに読めるような読めないような感じなんですね。

  • 読めるほう:inoutを引き合いに出し、コピーなしで渡すことができる。sharedは不変参照になる。というような話をしている
  • 読めないほう:Function parametersのところでは、現在、暗黙で決まる引数の渡し方ルールを、明示的に指定できるようにする、と書いてある

引数に関して調べた結果としては、まさに後者の状態になっているんですね。

一方で、Local ephemeral bindingsのところに書いてある機能はまだありません。もしそれが導入されれば、それが前者にあたるのかも知れません。
ただ後者のように書いてある以上、Local ephemeral bindingsが導入されても関数引数をconst T*にできるようにはならない気がしますね。

ではLocal ephemeral bindingsが導入されそうかどうかという話なんですが。

It is already a somewhat silly limitation that Swift provides no way to abstract over storage besides passing it as an inout argument. It's an easy limitation to work around, since programmers who want a local inout binding can simply introduce a closure and immediately call it, but that's an awkward way of achieving something that ought to be fairly easy.

この部分は以下のような話だと思うんです。

func doSomething() {
    var value = 42

    // クロージャを使用してローカルなinoutバインディングを作成
    { (inout localValue: Int) in
        localValue += 1
    }(&value)

    print(value) // 43
}

ですが、こういうことをしたいというような状況がよく分かりません。関数にconst T*(もしくはconst T&)を渡したいというのはよく分かるんですけども。

kabeyakabeya

@convention(thick)はどうなったか

結局、まだ@convention(thick)が何か分かっていません。
今までの例に出てきたのは@convention(thin)@convention(method)だけです。

他の@conventionを例で確認します。

func convention_c() {
    var t: time_t = 0
    time(&t) // これは@convention(c)で呼ばれる
    print(t)
}

func convention_block() {
    let array = NSArray()
    array.enumerateObjects {(obj,index,ioNeedToStop) in
        print(obj)
    } // このクロージャ(ブロック)は@convention(block)で呼ばれる 
}

func convention_obc_method() {
    let e = NSString()
    let s = e.appending("append") // これは@convention(objc_method)で呼ばれる
    print(s)
}

protocol DoSomething {
    func doSomething()
}

class Foo: DoSomething {
    func doSomething() {
        print("doSomething()")
    }        
}

func convention_witness_method() {
    let foo: DoSomething = Foo()
    foo.doSomething() // これは@convention(witness_method)で呼ばれる
}
kabeyakabeya

@convention(thick)は明示的には現れないのではないか

最初に挙げた呼び出し規約の表を見ると@convention(thick)は暗黙的なものであって、明示的に現れることは一切ないのではないかという気がしてきました。というか改めて読むとそうとしか読めません。
SILのドキュメントの別の箇所にも以下の記述があります。

  • If it is @callee_guaranteed, the context value is treated as a direct parameter. This implies @convention(thick). If the function type is also @noescape, then the context value is unowned, otherwise it is guaranteed.
  • If it is @callee_owned, the context value is treated as an owned direct parameter. This implies @convention(thick) and is mutually exclusive with @noescape.

もしそうであれば、コンテキストというのはクロージャに渡される諸々のデータ一通りのことだと思われるので、この@convention(thick)はクロージャ用ということになります。

func mainFunc() {
    let closure = { (value: Int) in
    }
    closure(3)
}

このコードのSILは以下の通りです。

SIL
001: // mainFunc()
002: sil hidden @$s6Test188mainFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   // function_ref closure #1 in mainFunc()
005:   %0 = function_ref @$s6Test188mainFuncyyFySicfU_ : $@convention(thin) (Int) -> () // user: %1
006:   %1 = thin_to_thick_function %0 : $@convention(thin) (Int) -> () to $@callee_guaranteed (Int) -> () // users: %5, %2
007:   debug_value %1 : $@callee_guaranteed (Int) -> (), let, name "closure" // id: %2
008:   %3 = integer_literal $Builtin.Int64, 3          // user: %4
009:   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %5
010:   %5 = apply %1(%4) : $@callee_guaranteed (Int) -> ()
011:   %6 = tuple ()                                   // user: %7
012:   return %6 : $()                                 // id: %7
013: } // end sil function '$s6Test188mainFuncyyF'

実際、クロージャは006行でthin_to_thick_functionにより、thin関数からthick関数に変換されて%1に入り、それを使って010行で呼び出しされています。
クロージャの型は@callee_guaranteed (Int) -> ()ということなので、これが@convention(thick)を意味する、ということなんでしょう。

@callee_ownedにする方法はまだ分かっていませんので、もう少し調べます。

kabeyakabeya

@callee_ownedはもう現れないのではないか

Swiftのソースコード内にテストコードもあります。
その中に以下のようなファイルがあります。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/test/SILGen/capture_list.swift

おそらく、これをコンパイルすると以下のような行ができるはず、というテストなんだろうと思うのですが。

%* = function_ref @$s12capture_list9transformxxyc2fn_tlF : $@convention(thin) <τ_0_0> (@owned @callee_owned () -> @out τ_0_0) -> @out τ_0_0

これをCompiler Explorerでコンパイルすると、以下のようになりました。

  • swiftc 3.1.1〜4.0.3:@callee_owned
  • swiftc 4.1〜5.5:@callee_guaranteed
  • swiftc 5.6〜:特になし

もう@callee_ownedというのは、生成されないんじゃないかという気がします。

ネット上にある、昔の@callee_owned絡みの書き込みを見ると、およそほとんどの場合reabstraction thunk helperに関係する話です。
SILのドキュメントでも@callee_ownedに触れているのは主にAbstraction Differenceの箇所です。

このドキュメントのAbstraction Differenceのところに例として挙がっているgenerateArray<T>について、もう少し考えてみます。

func generateArray<T>(n : Int, generator : () -> T) -> [T]

generateArray<T>generatorとして関数(関数ポインタ)を取ります。その上でそのgenerator関数は、暗黙的に確保されているメモリアドレスをTで埋めます。暗黙的に確保されている、というのが外側に確保されていて、関数でキャプチャされた(そしてコンテキストとして渡される)メモリ領域です。

it becomes @callee_owned () -> @out Int

とあります。このgenerator関数が呼ばれると、メモリ領域の管理権(所有権)が関数の外側から関数に移動します。必要に応じて関数内でそのメモリ領域を操作していいということです。これがおそらく@callee_owned(呼び出し先により所有される)の意味合いだろうと推測します。
ちなみに@outは、値を直接返すのではなくメモリ領域に埋めて返す、という意味合いです。

Swift 4.1からは、暗黙的に渡されるメモリ領域の所有権は呼び出し先に移すのではなく、呼び出し元では呼び出し先から戻ってくるまでメモリ領域の存在を保証する(@callee_guaranteed)ということに変わったのではないかと思います。

ただ、Swift 4.1の何がこの変化を起こさせたのかは、CHANGELOGを見ても分かりませんでした。
またそもそも、なぜ4.0までジェネリックの場合にだけ@callee_ownedの話が出ていたのかもよく分かりませんでした。

kabeyakabeya

この長かった旅路も、いったんここで終了にしたいと思います。
今回分かったことはまとめたうえで記事にします。
(こんだけ長いと自分でもまとめないとよく分からない)

kabeyakabeya

この長かった旅路も、いったんここで終了にしたいと思います。

こう書いたんですけども、ふと@callee_ownedがなくなっているのに@callee_guaranteeがそのままということがあるのかな?という気がしたので、もう少し調査することにしました。

Swift 3.1.1のアセンブラはCompiler Explorerで出力できますが、SILは出力できないので[1]、dockerでSwift 3.1.1を動かすことにしました。

Docker Desktopがインストールされている状態で、

$ docker run --platform linux/amd64 -it swift:3.1.1 /bin/bash

を実行すると、Swift 3.1.1がインストールされたUbuntu 16.04が起動して、bashプロンプトが表示されます。
--platform linux/amd64はインテルMacでは不要ですが、M1 Macではないと動きません。

脚注
  1. と思っていましたが、この投稿をする際に、「Compiler Options」フィールドに「-emit-sil」を付ければ出力できることに気付きました ↩︎

kabeyakabeya

@convention(thick)の説明には以下のように書いてあります。

関数が必要とするキャプチャやその他の状態を表すための、参照カウントされたコンテキストオブジェクトを持つ。

ということで、外側の変数をキャプチャしたようなクロージャを定義して、そのSILを見てみます。

class TestData1 {
    var value: Int = 3
    init(value: Int) {
        self.value = value
    }
}

class TestData2 {
    var value: Int = 3
    init(value: Int) {
        self.value = value
    }
}

func mainFunc(testData1: TestData1, testData2: TestData2) {
    let closure = { (value: Int) in
        testData1.value = value
        testData2.value = value
    }
    closure(3)
}

このmainFunc(testData1: testData2:)のSILを5.10→3.1.1の順に確認します。

001: // mainFunc(testData1:testData2:)
002: sil hidden @$s6Test198mainFunc9testData10D5Data2yAA04TestE0C_AA0gF0CtF : $@convention(thin) (@guaranteed TestData1, @guaranteed TestData2) -> () {
003: // %0 "testData1"                                 // users: %10, %9, %5, %2
004: // %1 "testData2"                                 // users: %11, %9, %6, %3
005: bb0(%0 : $TestData1, %1 : $TestData2):
006:   debug_value %0 : $TestData1, let, name "testData1", argno 1 // id: %2
007:   debug_value %1 : $TestData2, let, name "testData2", argno 2 // id: %3
008:   // function_ref closure #1 in mainFunc(testData1:testData2:)
009:   %4 = function_ref @$s6Test198mainFunc9testData10D5Data2yAA04TestE0C_AA0gF0CtFySicfU_ : $@convention(thin) (Int, @guaranteed TestData1, @guaranteed TestData2) -> () // user: %9
010:   strong_retain %0 : $TestData1                   // id: %5
011:   strong_retain %1 : $TestData2                   // id: %6
012:   %7 = integer_literal $Builtin.Int64, 3          // user: %8
013:   %8 = struct $Int (%7 : $Builtin.Int64)          // user: %9
014:   %9 = apply %4(%8, %0, %1) : $@convention(thin) (Int, @guaranteed TestData1, @guaranteed TestData2) -> ()
015:   strong_release %0 : $TestData1                  // id: %10
016:   strong_release %1 : $TestData2                  // id: %11
017:   %12 = tuple ()                                  // user: %13
018:   return %12 : $()                                // id: %13
019: } // end sil function '$s6Test198mainFunc9testData10D5Data2yAA04TestE0C_AA0gF0CtF'

確認すると、何も特別なことをしていないんですね。
クロージャも引数が3つある状態でfunction_refを作られていますし、それを引数3つ指定して呼ぶだけ。
コンテキストと言えるようなものは何も登場しませんし、渡してもいません。
シンプル。

続いて3.1.1です。

001: // mainFunc(testData1 : TestData1, testData2 : TestData2) -> ()
002: sil hidden @_TF8Test19_18mainFuncFT9testData1CS_9TestData19testData2CS_9TestData2_T_ : $@convention(thin) (@owned TestData1, @owned TestData2) -> () {
003: // %0                                             // users: %15, %7, %5, %2
004: // %1                                             // users: %14, %7, %6, %3
005: bb0(%0 : $TestData1, %1 : $TestData2):
006:   debug_value %0 : $TestData1, let, name "testData1", argno 1 // id: %2
007:   debug_value %1 : $TestData2, let, name "testData2", argno 2 // id: %3
008:   // function_ref (mainFunc(testData1 : TestData1, testData2 : TestData2) -> ()).(closure #1)
009:   %4 = function_ref @_TFF8Test19_18mainFuncFT9testData1CS_9TestData19testData2CS_9TestData2_T_U_FSiT_ : $@convention(thin) (Int, @owned TestData1, @owned TestData2) -> () // user: %7
010:   strong_retain %0 : $TestData1 // id: %5
011:   strong_retain %1 : $TestData2 // id: %6
012:   %7 = partial_apply %4(%0, %1) : $@convention(thin) (Int, @owned TestData1, @owned TestData2) -> () // users: %13, %12, %9, %8
013:   debug_value %7 : $@callee_owned (Int) -> (), let, name "closure" // id: %8
014:   strong_retain %7 : $@callee_owned (Int) -> () // id: %9
015:   %10 = integer_literal $Builtin.Int64, 3 // user: %11
016:   %11 = struct $Int (%10 : $Builtin.Int64) // user: %12
017:   %12 = apply %7(%11) : $@callee_owned (Int) -> ()
018:   strong_release %7 : $@callee_owned (Int) -> () // id: %13
019:   strong_release %1 : $TestData2 // id: %14
020:   strong_release %0 : $TestData1 // id: %15
021:   %16 = tuple () // user: %17
022:   return %16 : $() // id: %17
023: } // end sil function '_TF8Test19_18mainFuncFT9testData1CS_9TestData19testData2CS_9TestData2_T_'

ここで@callee_ownedが現れました。ジェネリックのときだけ現れる訳じゃないのか…という気になりましたね。
012行目では、TestData1TestData2だけの引数を渡してpartial_applyをしています。これがさらに014でstrong_retainされています。これのことなんでしょうか、参照カウンタを用いたコンテキストオブジェクトの保持とは。

予期せず@callee_ownedだったので、@callee_ownedがなくなったと思われるSwift 4.1のSILも見てみます(のはずが、docker run swift:4.1に入ってたSwiftが4.1.3でしたので4.1.3で)。

001: // mainFunc(testData1:testData2:)
002: sil hidden @_T08Test19_28mainFuncyAA9TestData1C04testE0_AA0D5Data2C0fG0tF : $@convention(thin) (@owned TestData1, @owned TestData2) -> () {
003: // %0                                             // users: %16, %7, %5, %2
004: // %1                                             // users: %15, %7, %6, %3
005: bb0(%0 : $TestData1, %1 : $TestData2):
006:   debug_value %0 : $TestData1, let, name "testData1", argno 1 // id: %2
007:   debug_value %1 : $TestData2, let, name "testData2", argno 2 // id: %3
008:   // function_ref closure #1 in mainFunc(testData1:testData2:)
009:   %4 = function_ref @_T08Test19_28mainFuncyAA9TestData1C04testE0_AA0D5Data2C0fG0tFySicfU_ : $@convention(thin) (Int, @guaranteed TestData1, @guaranteed TestData2) -> () // user: %7
010:   strong_retain %0 : $TestData1                   // id: %5
011:   strong_retain %1 : $TestData2                   // id: %6
012:   %7 = partial_apply [callee_guaranteed] %4(%0, %1) : $@convention(thin) (Int, @guaranteed TestData1, @guaranteed TestData2) -> () // users: %14, %13, %12, %9, %8
013:   debug_value %7 : $@callee_guaranteed (Int) -> (), let, name "closure" // id: %8
014:   strong_retain %7 : $@callee_guaranteed (Int) -> () // id: %9
015:   %10 = integer_literal $Builtin.Int64, 3         // user: %11
016:   %11 = struct $Int (%10 : $Builtin.Int64)        // user: %12
017:   %12 = apply %7(%11) : $@callee_guaranteed (Int) -> ()
018:   strong_release %7 : $@callee_guaranteed (Int) -> () // id: %13
019:   strong_release %7 : $@callee_guaranteed (Int) -> () // id: %14
020:   strong_release %1 : $TestData2                  // id: %15
021:   strong_release %0 : $TestData1                  // id: %16
022:   %17 = tuple ()                                  // user: %18
023:   return %17 : $()                                // id: %18
024: } // end sil function '_T08Test19_28mainFuncyAA9TestData1C04testE0_AA0D5Data2C0fG0tF'

@callee_owned@callee_guaranteedになっているぐらいでほぼ3.1.1と同じです…と思ったのですが、018〜019で2回strong_releaseしていますね。これが@callee_owned@callee_guaranteedの違いなんでしょうか。

ちょっと奥が深そうなのでいったん切ります。

kabeyakabeya

なぜ、@callee_ownedstrong_releaseが1回で、@callee_guaranteedは2回なのか。

まず3.1.1のアセンブラを見ます。

アセンブラ-3.1.1
001: output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         sub     rsp, 64
005:         mov     qword ptr [rbp - 8], rdi
006:         mov     qword ptr [rbp - 16], rsi
007:         mov     rax, rdi
008:         mov     qword ptr [rbp - 40], rdi
009:         mov     rdi, rax
010:         mov     qword ptr [rbp - 48], rsi
011:         call    swift_rt_swift_retain
012:         mov     rax, qword ptr [rbp - 48]
013:         mov     rdi, rax
014:         call    swift_rt_swift_retain
015:         lea     rax, [rip + .Lmetadata]
016:         add     rax, 16
017:         mov     ecx, 32
018:         mov     esi, ecx
019:         mov     ecx, 7
020:         mov     edx, ecx
021:         mov     rdi, rax
022:         call    swift_rt_swift_allocObject
023:         lea     rdx, [rip + partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()]
024:         mov     rsi, qword ptr [rbp - 40]
025:         mov     qword ptr [rax + 16], rsi
026:         mov     rdi, qword ptr [rbp - 48]
027:         mov     qword ptr [rax + 24], rdi
028:         mov     qword ptr [rbp - 32], rdx
029:         mov     qword ptr [rbp - 24], rax
030:         mov     rdi, rax
031:         mov     qword ptr [rbp - 56], rax
032:         call    swift_rt_swift_retain
033:         mov     ecx, 3
034:         mov     edi, ecx
035:         mov     rsi, qword ptr [rbp - 56]
036:         call    partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()
037:         mov     rdi, qword ptr [rbp - 56]
038:         call    swift_rt_swift_release
039:         mov     rdi, qword ptr [rbp - 48]
040:         call    swift_rt_swift_release
041:         mov     rdi, qword ptr [rbp - 40]
042:         call    swift_rt_swift_release
043:         add     rsp, 64
044:         pop     rbp
045:         ret
046: 
047: partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
048:         push    rbp
049:         mov     rbp, rsp
050:         sub     rsp, 32
051:         mov     rax, qword ptr [rsi + 16]
052:         mov     qword ptr [rbp - 8], rdi
053:         mov     rdi, rax
054:         mov     qword ptr [rbp - 16], rax
055:         mov     qword ptr [rbp - 24], rsi
056:         call    swift_rt_swift_retain
057:         mov     rax, qword ptr [rbp - 24]
058:         mov     rsi, qword ptr [rax + 24]
059:         mov     rdi, rsi
060:         mov     qword ptr [rbp - 32], rsi
061:         call    swift_rt_swift_retain
062:         mov     rdi, qword ptr [rbp - 24]
063:         call    swift_rt_swift_release
064:         mov     rdi, qword ptr [rbp - 8]
065:         mov     rsi, qword ptr [rbp - 16]
066:         mov     rdx, qword ptr [rbp - 32]
067:         add     rsp, 32
068:         pop     rbp
069:         jmp     closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()
070: 
071: closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
072:         push    rbp
073:         mov     rbp, rsp
074:         sub     rsp, 48
075:         mov     qword ptr [rbp - 8], rdi
076:         mov     qword ptr [rbp - 16], rsi
077:         mov     qword ptr [rbp - 24], rdx
078:         mov     rax, qword ptr [rsi]
079:         mov     qword ptr [rbp - 32], rdi
080:         mov     qword ptr [rbp - 40], rsi
081:         mov     qword ptr [rbp - 48], rdx
082:         call    qword ptr [rax + 88]
083:         mov     rax, qword ptr [rbp - 48]
084:         mov     rdx, qword ptr [rax]
085:         mov     rdi, qword ptr [rbp - 32]
086:         mov     rsi, rax
087:         call    qword ptr [rdx + 88]
088:         mov     rdi, qword ptr [rbp - 48]
089:         call    swift_rt_swift_release
090:         mov     rdi, qword ptr [rbp - 40]
091:         call    swift_rt_swift_release
092:         add     rsp, 48
093:         pop     rbp
094:         ret

022でswift_allocObjectをしています。これがstrong_retainされる「クロージャがpartial_applyされた」オブジェクトです。この段階で、参照カウントは1になっています。
このオブジェクトには025,027でそれぞれTestDataTestData2が設定されます。
032でswift_rt_swift_retain(=strong_retain)されますが、これにより参照カウントは2になります。

036でpartial apply forwarder for closure #1を呼びます。047に制御が移ります。引数は3つ、第1引数rdiと第2引数rsiはともに022のオブジェクトです。第3引数rcxは3です。
056でswift_rt_swift_retainされているのはTestData1です。同様に061でswift_rt_swift_retainされているのはTestData2です。
063でswift_rt_swift_releaseされているのは、022のオブジェクトです。これにより参照カウントは1になります。
069でclosure #1に飛びます。制御は071に移ります。089/091でTestData1/TestData2をそれぞれリリースします。

戻ってきて038で、022のオブジェクトをswift_rt_swift_releaseします。これで参照カウントは0になります。

ということで、3.1.1のほうは呼び出し先のクロージャでreleaseしていました。

kabeyakabeya

ついで4.1.2(Compiler Explorerに4.1.3がなかったので)のアセンブラを見ます。

アセンブラ-4.1.2
001: output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         sub     rsp, 88
006:         mov     qword ptr [rbp - 16], rdi
007:         mov     qword ptr [rbp - 24], rsi
008:         mov     qword ptr [rbp - 48], rdi
009:         mov     qword ptr [rbp - 56], rsi
010:         call    swift_rt_swift_retain
011:         mov     rdi, qword ptr [rbp - 56]
012:         mov     qword ptr [rbp - 64], rax
013:         call    swift_rt_swift_retain
014:         lea     rdi, [rip + .Lmetadata+16]
015:         mov     ecx, 32
016:         mov     esi, ecx
017:         mov     ecx, 7
018:         mov     edx, ecx
019:         mov     qword ptr [rbp - 72], rax
020:         call    swift_rt_swift_allocObject
021:         mov     rdx, qword ptr [rbp - 48]
022:         mov     qword ptr [rax + 16], rdx
023:         mov     rsi, qword ptr [rbp - 56]
024:         mov     qword ptr [rax + 24], rsi
025:         lea     rdi, [rip + partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()]
026:         mov     qword ptr [rbp - 40], rdi
027:         mov     qword ptr [rbp - 32], rax
028:         mov     rdi, rax
029:         mov     qword ptr [rbp - 80], rax
030:         call    swift_rt_swift_retain
031:         mov     ecx, 3
032:         mov     edi, ecx
033:         mov     r13, qword ptr [rbp - 80]
034:         mov     qword ptr [rbp - 88], rax
035:         call    partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()
036:         mov     rdi, qword ptr [rbp - 80]
037:         call    swift_rt_swift_release
038:         mov     rdi, qword ptr [rbp - 80]
039:         call    swift_rt_swift_release
040:         mov     rdi, qword ptr [rbp - 56]
041:         call    swift_rt_swift_release
042:         mov     rdi, qword ptr [rbp - 48]
043:         call    swift_rt_swift_release
044:         add     rsp, 88
045:         pop     r13
046:         pop     rbp
047:         ret
048: 
049: partial apply forwarder for closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
050:         push    rbp
051:         mov     rbp, rsp
052:         mov     rsi, qword ptr [r13 + 16]
053:         mov     rdx, qword ptr [r13 + 24]
054:         call    closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ()
055:         pop     rbp
056:         ret
057:         
058: closure #1 (Swift.Int) -> () in output.mainFunc(testData1: output.TestData1, testData2: output.TestData2) -> ():
059:         push    rbp
060:         mov     rbp, rsp
061:         push    r13
062:         sub     rsp, 40
063:         mov     qword ptr [rbp - 16], rdi
064:         mov     qword ptr [rbp - 24], rsi
065:         mov     qword ptr [rbp - 32], rdx
066:         mov     rax, qword ptr [rsi]
067:         mov     rax, qword ptr [rax + 88]
068:         mov     qword ptr [rbp - 40], rdi
069:         mov     r13, rsi
070:         mov     qword ptr [rbp - 48], rdx
071:         call    rax
072:         mov     rax, qword ptr [rbp - 48]
073:         mov     rdx, qword ptr [rax]
074:         mov     rdx, qword ptr [rdx + 88]
075:         mov     rdi, qword ptr [rbp - 40]
076:         mov     r13, rax
077:         call    rdx
078:         add     rsp, 40
079:         pop     r13
080:         pop     rbp
081:         ret

020でswift_rt_swift_allocObjectして、TestData1/TestData2のポインタをそのオブジェクトにセットして、030でswift_rt_swift_retainして、というところまでは3.1.1とほぼ同じです。

違うのはpartial apply forwarder for closure #1でもclosure #1でも、retain/releaseを一切していないというところです。TestData1/TestData2retain/releaseもなくなっています。

結果として、@callee_ownedのときと異なり、クロージャを抜けた時点で020のオブジェクトの参照カウントは2のままです。
このため2回のreleaseが必要になってしまいます。

kabeyakabeya

というわけで。

@convention(thick)相当のものは@callee_ownedにせよ@callee_guaranteedにせよ、過去にはあったけども現在は出力されない、ということではないかと推測します。

SIL.rstに記述されているのは「コンテキストデータを参照カウントで管理する」コードですが、現在のSwiftはもっとシンプルなコードが生成されます。

partial_applyのような処理(カリー化?)も行われなくなっているような気がします。

もともと「ジェネリックなコードから特定の型ごとにコードが生成されるのを避ける」というのが目的だとしても、それが効いてくる「単一のジェネリックなコードから様々な型のコードが生成される」というケースはほとんどない、というのが現実のように思います。せいぜい1個か2個の型が使われるぐらいで。

いよいよSwift 6の時代が来ますが、何というか、コンカレンシー周りも上記の話と似たような臭いがします。最初は理想を追い求めてしまって制約や実装が重いんだけれども、使う人・ケースが増えノウハウが蓄積されるにつれ、適度な落とし所に落ち着くというような。SE-0401なんかは、まさにそういうものという感じがします。

kabeyakabeya

まとめを書こうとして調べ物をしていたら以下のようなページに行き当たりました。

https://qiita.com/yusuga/items/4983b5282de195c39d35

  • クロージャへは参照渡し、キャプチャリストを使えば値渡し
  • 関数へは値渡し、inoutを使えば 参照渡し write-back

クロージャにstructを暗黙で渡すコードは検証していませんでした。検証はclassだけやっていました。
キャプチャリストも試していませんでした。
その2つはやるとして。
なんでしょう、write-back。

https://gist.github.com/es-kumagai/228e466903f5394baf8f68c7ce865c9a

ここのコードのことでしょうか。

kabeyakabeya

とりあえず、以下のコードのSILやアセンブラを見てみます。

Swift
func testFunc() {
    var value: Int = 7
    let r = withUnsafeMutablePointer(to: &value) { p in
        p.pointee = 18
        return 3.0
    }
}
SIL-testFunc
001: // testFunc()
002: sil hidden @$s6Test228testFuncyyF : $@convention(thin) () -> () {
003: bb0:
004:   %0 = alloc_stack $Int, var, name "value"        // users: %3, %15, %7
005:   %1 = integer_literal $Builtin.Int64, 7          // user: %2
006:   %2 = struct $Int (%1 : $Builtin.Int64)          // user: %3
007:   store %2 to %0 : $*Int                          // id: %3
008:   %4 = alloc_stack $Double                        // users: %12, %14, %9
009:   // function_ref closure #1 in testFunc()
010:   %5 = function_ref @$s6Test228testFuncyyFSdSpySiGXEfU_ : $@convention(thin) @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <Int, Double> // user: %6
011:   %6 = thin_to_thick_function %5 : $@convention(thin) @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <Int, Double> to $@noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <Int, Double> // user: %9
012:   %7 = begin_access [modify] [static] %0 : $*Int  // users: %11, %9
013:   // function_ref withUnsafeMutablePointer<A, B>(to:_:)
014:   %8 = function_ref @$ss24withUnsafeMutablePointer2to_q_xz_q_SpyxGKXEtKr0_lF : $@convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed @noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <τ_0_0, τ_0_1>) -> (@out τ_0_1, @error any Error) // user: %9
015:   try_apply %8<Int, Double>(%4, %7, %6) : $@convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed @noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <τ_0_0, τ_0_1>) -> (@out τ_0_1, @error any Error), normal bb1, error bb2 // id: %9
016: 
017: bb1(%10 : $()):                                   // Preds: bb0
018:   end_access %7 : $*Int                           // id: %11
019:   %12 = load %4 : $*Double                        // user: %13
020:   debug_value %12 : $Double, let, name "r"        // id: %13
021:   dealloc_stack %4 : $*Double                     // id: %14
022:   dealloc_stack %0 : $*Int                        // id: %15
023:   %16 = tuple ()                                  // user: %17
024:   return %16 : $()                                // id: %17
025: 
026: bb2(%18 : $any Error):                            // Preds: bb0
027:   unreachable                                     // id: %19
028: } // end sil function '$s6Test228testFuncyyF'

ポイントは015行です。クロージャの返値用%4、排他アクセスチェック付きのvalueに対するポインタ%7、クロージャ%6の3つを引数にwithUnsafeMutablePointerを呼んでいます。

SIL-クロージャ
001: // closure #1 in testFunc()
002: sil private @$s6Test228testFuncyyFSdSpySiGXEfU_ : $@convention(thin) @substituted <τ_0_0, τ_0_1> (UnsafeMutablePointer<τ_0_0>) -> (@out τ_0_1, @error any Error) for <Int, Double> {
003: // %0 "$return_value"                             // user: %12
004: // %1 "p"                                         // users: %5, %2
005: bb0(%0 : $*Double, %1 : $UnsafeMutablePointer<Int>):
006:   debug_value %1 : $UnsafeMutablePointer<Int>, let, name "p", argno 1 // id: %2
007:   %3 = integer_literal $Builtin.Int64, 18         // user: %4
008:   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %8
009:   %5 = struct_extract %1 : $UnsafeMutablePointer<Int>, #UnsafeMutablePointer._rawValue // user: %6
010:   %6 = pointer_to_address %5 : $Builtin.RawPointer to [strict] $*Int // user: %7
011:   %7 = begin_access [modify] [unsafe] %6 : $*Int  // users: %8, %9
012:   store %4 to %7 : $*Int                          // id: %8
013:   end_access %7 : $*Int                           // id: %9
014:   %10 = float_literal $Builtin.FPIEEE64, 0x4008000000000000 // 3 // user: %11
015:   %11 = struct $Double (%10 : $Builtin.FPIEEE64)  // user: %12
016:   store %11 to %0 : $*Double                      // id: %12
017:   %13 = tuple ()                                  // user: %14
018:   return %13 : $()                                // id: %14
019: } // end sil function '$s6Test228testFuncyyFSdSpySiGXEfU_'

クロージャ側では%1UnsafeMutablePointer(=p)、%5p.pointee%6が生のポインタ、%7は排他アクセスチェック付きのポインタ、のような感じになっていて、p.pointeeの指す先を直接書き換えているように見えます。
アセンブラも見ます。

アセンブラ-testFunc
001: output.testFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         push    r13
005:         push    r12
006:         sub     rsp, 32
007:         mov     qword ptr [rbp - 24], 0
008:         xor     eax, eax
009:         mov     r12d, eax
010:         mov     qword ptr [rbp - 48], 0
011:         mov     qword ptr [rbp - 24], 7
012:         lea     rsi, [rip + (closure #1 (Swift.UnsafeMutablePointer<Swift.Int>) -> Swift.Double in output.testFunc() -> ())]
013:         mov     rcx, qword ptr [rip + ($sSiN)@GOTPCREL]
014:         mov     r8, qword ptr [rip + ($sSdN)@GOTPCREL]
015:         lea     rax, [rbp - 32]
016:         lea     rdi, [rbp - 24]
017:         mov     rdx, r12
018:         call    ($ss24withUnsafeMutablePointer2to_q_xz_q_SpyxGKXEtKr0_lF)@PLT
019:         cmp     r12, 0
020:         jne     .LBB1_2
021:         movsd   xmm0, qword ptr [rbp - 32]
022:         movsd   qword ptr [rbp - 48], xmm0
023:         add     rsp, 32
024:         pop     r12
025:         pop     r13
026:         pop     rbp
027:         ret
028: .LBB1_2:
029:         xor     eax, eax

rdiレジスタには、valueのポインタ(rbp-24のアドレス)を入れています。

クロージャ
001: closure #1 (Swift.UnsafeMutablePointer<Swift.Int>) -> Swift.Double in output.testFunc() -> ():
002:         push    rbp
003:         mov     rbp, rsp
004:         mov     qword ptr [rbp - 8], 0
005:         mov     qword ptr [rbp - 8], rdi
006:         mov     qword ptr [rdi], 18
007:         movabs  rcx, 4613937818241073152
008:         mov     qword ptr [rax], rcx
009:         pop     rbp
010:         ret

rdiレジスタの指すアドレスに、18を入れています。

もし、withUnsafeMutablePointerがレジスタを書き換えてないのであれば、クロージャはvalueのメモリ領域を直接書き換えているということになります。

問題はwithUnsafeMutablePointerが何をやっているかですね。

kabeyakabeya

ちなみに。

func printValue(_ value: Int) {
    print("value: \(value)")
}

func testFunc() {
    var value: Int = 7
    let r = withUnsafeMutablePointer(to: &value) { p in
        printValue(value)
        p.pointee = 18
        printValue(value)
        return 3.0
    }
}

上記のように、withUnsafeMutablePointerに渡した変数をクロージャの中で参照・更新するコードは、現在のSwiftではコンパイルエラーになります。

このコードがコンパイルできた当時でも、もともとが想定されていないvalueへのアクセス方法だと思います。
このため、以下のいずれかの状況が発生していたのではないかと想像します。

  • 18を代入する処理の結果がレジスタに載っているものの、まだメインメモリには入っていない
  • メインメモリとレジスタの両方にvalueの7が載っていて、18の代入はメインメモリを直接書き換えているが、valueprintはレジスタの7から出力している

このいずれかにより結果としてwrite backのように見えていただけではないかと推測します。
(Swift 3.0.2ならコンパイルできるのかと思って試しましたができませんでした)