Swift: 呼び出し規約を調べる
「Swiftポインタ入門」という本を書いています(書いてる最中です)。
そのなかで「関数を呼び出す場合、引数は呼び出し元の変数から呼び出し先の変数にコピーされます」というように書いたのですが、本当かな?ということで改めて調べ直そうと思います。
- Swiftで関数を呼ぶ際、どのようなことが起きているのか
- 型や関数で違いがあるか
これらの点について、コンパイラの出力を見て確認することにします。
最初は全部調べきってから記事にしようかと思っていたんです。
ですが、間違って理解している箇所もあって戻ったりしているので、その調査とかやり直しの過程込みで徐々に書いていった方がいいのかなという気がしてきました。
まとまったら記事にします。
Swiftの呼び出し規約について
Swiftの呼び出し規約は、The Swift Calling Conventionに記述があります。
上記のドキュメントによれば「Swiftの呼び出し規約」は、大きく3レベルに分けられます。
- 高級言語レベルでの呼び出し規約:主に参照による引数渡し vs 値による引数渡しの話
- SIL(Swift Intermediate Languate)レベルでの呼び出し規約:引数や返値の所有権移転、retain/release/コピーに関するもの
- マシン語(≒アセンブラ言語)レベルでの呼び出し規約:レジスタやメモリの使用方法に関するもの
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の部分の出力(のアセンブラ)を確認します。
出力の確認方法
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])。
-
-target x86_64-apple-macos13
がないと、Apple Silicon Macではarm64のアセンブラが出力されます ↩︎ -
Addressing Architectural Differences in Your macOS Code (https://developer.apple.com/documentation/apple-silicon/addressing-architectural-differences-in-your-macos-code) ↩︎
高級言語レベルでの呼び出し規約
「Swiftの呼び出し規約は、The Swift Calling Conventionに記述があります」とは書きましたが、実際、これには仕様がほとんど書かれていません(結論もあるんだかないんだか分からないような書き方ですし)。
現在の最新言語仕様が統一的に書かれているドキュメントが見当たらないような気がします(探せていないだけかも知れませんが)。
だからこそ、ここで調べようとしています。
調査開始時点で気付いている点は以下です。
- 引数の渡しかた
- 参照による引数渡しと値による引数渡しがある
- 引数ごとに渡し方は異なる
-
inout
/borrowing
/consuming
のパラメータ修飾子があり動きが変わる - 参照型と値型で動きが異なる
-
Copyable
な型と~Copyable
な型で制約が変わる -
let
とvar
で引数の渡し方が異なる - 「イニシャライザ、プロパティセッタ」と「それ以外の関数」で動きが変わる
- 引数にはSwift独自のエイリアシングルールがある(今回は触れません)
- 返値の受け取りかた
- 原則、値による返値渡し
- 返値がない場合も、空のタプルが返ってきていて、それを受け取ることができる
- その他
- 関数には
mutating
/consuming
/borrowing
の修飾子を付けることができ、いずれもself
に関する制約が変わる(今回は触れません)
- 関数には
今回は、返値は深追いしないことにします。必要があれば多少触れるかも知れません。
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になります。
引数・返値の所有権規約
所有権の種類として、「None」「Owned」「Guaranteed」「Unowned」「Any」があります。
SILレベルの引数には「@owned
」「@guaranteed
」「@unowned
」のマークが付くことがあります。
ドキュメントには@none
というキーワードも出てはいるのですが、それが実際に使われるかどうかまでは分かりませんでした。
マシン語(アセンブラ)レベルの呼び出し規約
サマリ版が「Calling Convention Summary」にあります。
サマリでないものについては以下の通りです。
- Apple platformのx86呼び出し規約:Writing 64-bit Intel code for Apple Platforms
概ねSystem V psABI for AMD64に従っている、と書かれています。 - Apple platformのARM64呼び出し規約:Writing ARM64 code for Apple platforms
概ねProcedure Call Standard for the ARM 64-bit Architecture (AAPCS64)に従っている、と書かれています。
今回はARM64には触れずにx86だけ見ていくことにします。
x86_64の呼び出し規約
ざっくり言うと、以下のようになります。
- 整数型の引数を
rdi
→rsi
→rdx
→rcx
→r8
→r9
の6個のレジスタに引数順に入れる - 浮動小数点数の引数を
xmm0
〜xmm7
の8個のレジスタに引数順に入れる - 余った引数はスタックに引数の逆順に積む(後ろの引数が最初にスタックに積まれる。これによりスタックから取り出すとき、後ろの引数が最後に取り出される)
- 返値は
rax
→rdx
→rcx
→r8
レジスタ(浮動小数点数はxmm0
〜xmm8
、st0
、st1
)を使って返す
確認
ここからは実際にコードを書いてコンパイルして出力を確認します。
確認したい組み合わせ
少なくとも高級言語レベルでの規約で触れた「引数の渡し方」の組み合わせは調べたいと思います。
- 参照による引数渡し vs 値による引数渡し
-
inout
vsborrowing
vsconsuming
vs なし - 参照型 vs 値型(ではなくてthick vs thinなのかも)
-
let
vsvar
- 「イニシャライザ、プロパティセッタ」 vs 「それ以外の関数」
Copyable
と~Copyable
の件は制約だけの話なので、いったん今回の調査(アセンブラの出力まで確認)の対象からは除外します。
また2×4×2×2×2=64通りありますので、何かは省略するかも知れません。
(いま想定しているのは、let
とvar
は一部の組み合わせだけ調べれば充分そう、ということです。それ以外にもあるかも知れません)
最初の調査
最初の調査は、シンプルなものということで以下を調査します。
- 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)
}
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: 変数%1
にTestData
の型情報を入れる
006: 変数%2
に整数リテラル3
を入れる
007: 変数%3
にInt
構造体(3
)を入れる
008: 変数%4
に整数リテラル4
を入れる
009: 変数%5
にInt
構造体(4
)を入れる
011: 変数%6
にTestData.init(value1:value2:)
の関数ポインタを入れる
012: TestData.init(value1:value2:)
を%6
の関数ポインタにより呼び出す。引数はInt
構造体(3
、4
)と、TestData
の型情報。クラスメソッドの呼び出しになっている
012: TestData.init(value1:value2:)
により返ってきたTestData
は変数%7
に入る
013: %0
の指すアドレスに確保されているメモリ領域に、%7
に入っている初期化されたTestData
を入れる
015: 変数%9
にtestFunc(_:)
の関数ポインタを入れる
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: 変数%3
にTestData.value2
の値を入れる
008: グローバル変数globalValue
へのアクセスを開始する。[dynamic]
は、このアクセスが排他的に行われているか実行時にチェックすることを示す
008: begin_access
で得られたポインタを変数%4
にセットする
009: 変数%3
の値を変数%4
のポインタの指すアドレスに入れる
010: begin_access
で得られたポインタへのアクセスを終了する
012: 空のタプルを返す。
testData:
を受け取ったときにさらに何かするということもなく、受け取ったまま使っています。
アセンブラ
続いてアセンブラを順に見ていきます。まず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
の関数プロトタイプは以下です。
void swift::swift_beginAccess(void *pointer, ValueBuffer *buffer,ExclusivityFlags flags, void *pc);
void swift::swift_endAccess(ValueBuffer *buffer);
第1引数のpointer
はグローバル変数などアクセス制御対象変数のアドレスです。rdiを使って渡します。
第2引数のbuffer
はswift_endAccess
と対になる作業用のバッファです。rsiを使って渡します。
第3引数のflags
はアクセスの排他性を示す定数です。rdxを使って渡します。
第4引数のpc
はプログラムカウンタです。次の実行命令のアドレスを指します。これをNULL(=0)で渡すと、このswift_beginAccess
の呼び出しの直後になります(のではないかと想像します。調べ切れていません)。rcxを使って渡します。
第3引数に指定されているExclusivityFlags
の定義は以下です。
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
を呼び出します。
まとめ
今回は以下のケースで調査しました。
- thin
- var
- 「イニシャライザ、プロパティセッタ」以外の関数
- パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。今回は64ビット値2つ分なのでレジスタだけで収まっているけども、もっと要素が多い場合はスタックにもコピーされる
- 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする(とは言え、たまたまかも。もう少し調査が必要)
調査の部分の長さのわりに、まとめがたいしたことなくて済みません。
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)
}
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
のときと差があるがアセンブラレベルでは一致してしまった。ただし例が簡単すぎる可能性もある。もう少し調査が必要
結果からさかのぼって、例の作り方を再考する必要がありますね。
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
}
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: 変数%10
にFoo
の型情報を入れる
017: 変数%11
にFoo.init(_:)
の関数ポインタを入れる
018: Foo.init(_:)
を%11
の関数ポインタにより呼び出す。引数は%7
に入っている初期化されたTestData
と、Foo
の型情報。クラスメソッドの呼び出しになっている
018: Foo.init(_:)
により返ってきたFoo
は変数%12
に入る
019: %9
の指すアドレスに確保されているメモリ領域に、%12
に入っている初期化されたFoo
を入れる
020: 変数%14
に整数リテラル5
を入れる
021: 変数%15
にInt
構造体(5
)を入れる
022: %9
の指すインスタンスへのアクセスを開始する。[static]
は、このアクセスが排他的に行われているかコンパイル時にのみチェックして実行時にはチェックしないことを示す
023: %17
にFoo
のインスタンスの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: 変数%1
にFoo
の型情報を入れる
006: Foo
のメモリ領域をスタックに確保し、%2
にそのアドレスを入れる
008: 変数%4
に整数リテラル0
を入れる
009: 変数%5
にInt
構造体(0
)を入れる
010: 変数%6
にFoo
構造体(0
)を入れる
011: %2
の指すアドレスに確保されているメモリ領域に、%6
に入っている初期化されたFoo
を入れる
012: 変数%8
に%0
のtestData
からvalue2
を取得して入れる
013: %2
の指すインスタンスへのアクセスを開始する。[static]
は、このアクセスが排他的に行われているかコンパイル時にのみチェックして実行時にはチェックしないことを示す
014: %10
にFoo
のインスタンスのpropValue
プロパティのアドレスを入れる
015: %10
のアドレスに%8
の値(=testData.value2
)を入れる
016: Foo
のインスタンスへのアクセスを終了する
017: 変数%13
にFoo
構造体(変数%8
で初期化したもの)を入れる
018: 確保したFoo
のメモリ領域を開放する
019: 変数%13
を返す
なんでしょうか、奇妙です。
006でalloc_stack
した変数は、019で返すのに使っていません。スタックに確保された領域なのでそれ自体を返さないのは分かりますが、017で%2
が使われていないのが不思議です。
これはアセンブラがどうなっているのか興味あります。
アセンブラ
アセンブラを順に見ていきます。まず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の無駄な処理が切り落とされるのでしょうか。
-
あとで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.」と書いてありました。 ↩︎
まとめ
今回は以下のケースで調査しました。
- thin
- var
- 「イニシャライザ、
プロパティセッタ」以外の関数 - パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。今回は64ビット値2つ分なのでレジスタだけで収まっているけども、もっと要素が多い場合はスタックにもコピーされる
- 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする(とは言え、たまたまかも。もう少し調査が必要)
- イニシャライザとそれ以外とで、特に大きな違いはなさそう
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)
}
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)
になる気もしますね。なんでしょう。
いったんthin関数/thick関数の話は置いておいてSILに戻ります。
全体の大まかな流れは最初の例と同じですが、何点か違いがあります。
- 関数ポインタの型の返値に
@owned
がついている(011) - 関数ポインタの型の引数に
@guaranteed
がついている(016) -
strong_retain
、strong_release
の呼び出しがある(013、018) -
destroy_addr
の呼び出しがある(019)
1については、TestData
クラスのイニシャライザで返ってきたオブジェクトは呼び出し元が所有するんですよ、というような意味合いでしょう。
実際、このイニシャライザから返ってきた012の段階で参照カウンタは1になっています。
その後、013でstrong_retain
しますが、これによって参照カウンタは2になります。
2については、testFunc(_:)
を呼び出している間はずっと引数のTestData
の存在が保証されるよ、という意味合いです。
呼び出しから返ってきたら、strong_release
で参照カウンタを下げます。これにより参照カウンタは1になります。
ややこしいのは4のdestory_addr
です。こいつは「引数のアドレスの指すものが自明型(ポインタを含まない型。整数など)なら何もせず、非自明型ならそのアドレスからオブジェクトをロードして、strong_release
をします」みたいなことがドキュメントには書いてあります。
ですが、~Copyable
のstruct
の場合、こいつはdeinit
を呼び出します。struct
でstrong_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つ言えるのは、やはりここでも参照カウンタの操作はしていない、ということです。
アセンブラ
アセンブラを順に見ていきます。まず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回あります。
次は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で初期化して返します。
ついで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同様、特別なことはしていません。
最後、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同様、参照カウンタの操作はしていません。
まとめ
今回は以下のケースで調査しました。
- thick
- var
- 「イニシャライザ、プロパティセッタ」以外の関数
- パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒に
self
もレジスタに入れて渡す。 - 渡す前に参照を
retain
する - 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
- 呼び出し先では参照カウンタの操作はしない
- 呼び出し後、呼ぶ前に
retain
した分のrelease
をする
こんな感じでしょうか。
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
でも差がない気はしますが、どうなるでしょうか。
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
のときのvar
→let
でも起こったことです。ポインタ変数でも同じことなんですね。
また、後者が予想外でした。参照型はメンバ関数にmutating
のキーワードがなく、let
で渡してもプロパティを更新できます。それでもretain
/release
が減るんですね。
と言いますかvar
のときにretain
/release
のあるのが予想外なのかも知れませんけども。
次いで、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
のときと同じです。
最後、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
のときと同じです。
アセンブラ
アセンブラを順に見ていきます。まず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_retain
とswift_release
の呼び出しだけがなくなっています。それ以外は同じです。
SILはスタックの変数の確保に違いがありましたが、アセンブラになるとその部分の差はなくなります。struct
のvar
→let
の場合も、SILだけ差があってアセンブラは完全に一致していましたので、これは予想通りとも言えます。
次は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
と完全に同じです。
最後、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
と同じです。
まとめ
今回は以下のケースで調査しました。
- thick
-
var→ let - 「イニシャライザ、プロパティセッタ」以外の関数
- パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒に
self
もレジスタに入れて渡す。 -
var
のときと異なり、渡す前に参照をretain
しない - 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
- 呼び出し先では参照カウンタの操作はしない
- 呼び出し後、呼ぶ前に
retain
していないので余分なrelease
はしない。イニシャライザで返ってきた時点の分のrelease
のみ
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
}
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'
全ステップ読み解くのがだんだんつらくなっているので、特徴的なポイントだけ列挙します。
-
TestData.init(value1:value2:)
から返ってきた参照(参照カウント1)を013でstrong_retain
している(参照カウント1→2) - 028で
destroy_addr
している(参照カウントが1つ減る) - (
var
を普通の関数に渡していた)4個目の調査のときにはdestory_addr
の直前に存在したstrong_release
がなくなっている
つまりここだけ見るとリークするんですね。この関数自体では参照カウントが1余っているんです。
ついで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個目の調査と完全に同じです。
続いて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点です。
-
.value2
がstruct_extract
ではなく、TestData.value2.getter
呼び出しでの取得になっている - 019で
testData
をstrong_release
している
2については、mainFunc()
で1余っていたように見えた参照カウントが、ここでstrong_release
により1減っているんですね。
これによってmainFunc()
の終わりのdestroy_addr
で参照カウントが0になり、解放されるということになります。
通常関数とイニシャライザでは、参照カウンタの操作が違うということが分かります。
次は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
次は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個目のときとの違いは以下です。
-
testData.value2
を取得するのに、TestData.value2.getter
の関数呼び出しを014で行っている - 関数呼び出しは
override
があるので、013の関数テーブル(rax+96)経由で行っている - 018で
swift_release
している - 関数内に、上記の2つの関数呼び出しがあるのでスタックポインタをセットしている
まとめ
今回は以下のケースで調査しました。
- thick
- var
- 「イニシャライザ、
プロパティセッタ」以外の関数 - パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数(参照)は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る。渡るのは参照。一緒に
self
もレジスタに入れて渡す - 渡す前に参照を
retain
する - 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
- 呼び出し先(イニシャライザ)では参照を
release
する - 呼ぶ前に
retain
しているが、呼び出し後にその分のrelease
はしない。イニシャライザで返ってきた時点の分のrelease
のみ
7個目の調査
struct
にString
を入れてみます。
- 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)
}
ここからは少し簡略化して書きます。
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の特徴は以下です。
- 018で
retain_value
という呼び出しが発生 - 同様に023で
release_value
という呼び出しが発生 - 024では
Int
だけのstruct
のときにはなかったdestroy_addr
も発生 - 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は特に目立った特徴がありませんでした。
アセンブラ-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()
のアセンブラで特徴的な箇所は以下になります。
- 020で
swift_bridgeObjectRetain
というのが呼ばれている - 同様に029で
swift_bridgeObjectRelease
というのが呼ばれている - 031で
(outlined destory of output.TestData)
というのが呼ばれている
swift_bridgeObjectRetain
というのは別のスクラップで詳しく追いかけましたが、ざっくりいうと引数のポインタの指すオブジェクトがObjective-CのオブジェクトなのかSwiftのオブジェクトなのか分からない場合でも、適切にretain
する、という関数です。
swift_bridgeObjectRelease
はそれのrelease
です。
(outlined destory of output.TestData)
は詳しく調べないといけません。
ただその前に、(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)
- 030で、rbpから32バイト引いたアドレスがrdiレジスタに入れられている
- rbpから32バイト引いたアドレスには、024でrdiレジスタの値が入れられている
- 024時点のrdiレジスタの値は、021で入れられた、rpbから56バイト引いたアドレスにある値
- rpbから56バイト引いたアドレスには016でraxレジスタの値が入れられている
- 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レジスタにはその最初の値を入れたメモリのアドレスが入ります。
では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引数そのもの、です。
ということで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つ使う型だということです。
String
のソースコードを見てみます。
String
のメンバ変数はvar guts: _StringGuts
だけです。
_StringGuts
は以下で定義されています。
_StringGuts
のメンバ変数もinternal var _object: _StringObject
だけです。
_StringObject
は以下で定義されています。
ちょっと色んなプラットフォーム向けが混在してあれですが、_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
です。
-
このポインタのフラグのうちの上位4ビットが、
_StringObject
のコメントに記載されているとおりに使われています ↩︎
というわけで、(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
に対して行っています。
何の話をしてたんでしたっけ、という感じではありますが、次は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()
のアセンブラも特に目立った特徴がありませんでした。
まとめ
今回は以下のケースで調査しました。
- thick?(
struct
内にString
を持たせたもの) - var
- 「イニシャライザ、プロパティセッタ」以外の関数
- パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
- 渡す前に
String
の内部のオブジェクトをretain
する - 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
- 呼び出し先では
String
の内部のオブジェクトの参照カウンタは操作しない - 呼ぶ前に
retain
したString
の内部のオブジェクトはrelease
し、さらにイニシャライザで返ってきた時点の分のrelease
もする
要は、struct
の内部のオブジェクトに参照があれば、class
のときと同じような参照カウンタ操作をする、ということです。
8個目の調査
7個目同様、struct
にString
を入れたものでやります。
- thick?
-
varlet - 「イニシャライザ
、プロパティセッタ」以外の関数 - パラメータ修飾子なし
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
を持たせたもの) -
varlet - 「イニシャライザ
、プロパティセッタ」以外の関数 - パラメータ修飾子なし
SIL、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数は呼び出し元でメモリからレジスタにコピーされて、呼び出し先に渡る
- 渡す前に
String
の内部のオブジェクトをretain
しない - 呼び出し先ではレジスタからいったんローカル変数にコピーするような動きをする
- 呼び出し先では
String
の内部のオブジェクトをrelease
する - 呼び出し側では、いっさい
release
しない
8個目の調査で、let
で宣言した変数を引数にしてイニシャライザを呼ぶ場合、呼び出し側でいっさいrelease
しないことが分かりました。
ということは、6個目の調査のまとめで「呼ぶ前にretain
しているが、呼び出し後にその分のrelease
はしない。イニシャライザで返ってきた時点の分のrelease
のみ」と書きましたが、これは間違っていますね。
イニシャライザで返ってきた分をrelease
しないんですね。6個目でrelease
しているのは、呼び出し前にretain
した分のみ、ということになります。
イニシャライザを呼ぶ場合、let
だろうがvar
だろうが、とにかく呼び出し側から呼び出し先(=イニシャライザ)に所有権が移る動きになっています。
ここまでの調査まとめ
引数の型 | 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 | なし | なし | なし | あり |
括弧書きの部分は他からの類推になります。
次の調査の前に
呼び出し元でrelease
がなくなり代わりに呼び出し先にrelease
が追加されるのは、consuming(消費)呼び出し規約と言います。
呼び出し先でretain
もrelease
もしないのは、borrowing(借用)呼び出し規約と言います。
Swiftは関数のタイプによって以下のように自動で規約を選んでいます。
- イニシャライザ、プロパティセッタ:consuming
- それ以外の関数:borrowing
パラメータ修飾子のconsuming
、borrowing
は、使用する規約を開発者側で明示的に変更するためのものです。
次以降の調査では、これらのパラメータ修飾子を付けるとどう変わるか見ていきます。
class
→struct
(String
あり)→struct
(Int
のみ)→struct
(Int
のみ、~Copyable
)の順で見ます。
最初のほうで~Copyable
は見ません、とか書いてましたが、やっぱり見たほうがいいですね。
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
しない
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
する
class→struct(Stringあり)→struct(Intのみ)→struct(Intのみ、~Copyable)の順で見ます。
と書きましたが、struct
のString
ありはおそらくclass
と同じ話にしかならないと思いますし、struct
のInt
のみはもともと一般関数とイニシャライザで違いがないので、おそらくconsuming
やborrowing
つけても違いがないので、struct
のInt
のみかつ~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]。
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()
では旧globalValue
のdeinit
が呼ばれ、testData
のdeinit
は呼ばれません。
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_extract
→struct_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、アセンブラの両方からの動きをまとめると、このケースの場合は以下のようになります。
- 引数は呼び出し元で変数のアドレスがレジスタにコピーされて、呼び出し先に渡る
- 呼び出し先ではレジスタにあるポインタを使って直接アクセスする。ただしポインタはローカル変数にもコピーする
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]。
15個目の調査(脱線)
ちょっと脱線なんですけれども、borrowing
、consuming
の代入・コピーの制約を調査します。
Swiftだけで確認します。
No. | パラメータ修飾子 | Copyable/~Copyable |
---|---|---|
15_1 | borrowing | Copyable |
15_2 | borrowing | ~Copyable |
15_3 | consuming | Copyable |
15_4 | consuming | ~Copyable |
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
}
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
}
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 // ←これもエラーにならない
}
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
されているのに…
もう少しよく調べると、イニシャライザ/プロパティセッタかそれ以外か、という話ではない気がしてきました。
Ownershipには、nonmutating
な関数のself
、mutating
な関数のself
という書き方がしてあります。しかもmutating
なら参照による引数渡しになるというんですね。
このドキュメント自体は、Swiftの言語をどうしてくかというような方向性の話を語るものであって、現在および未来の仕様を定義するものではないのですが、実際現行の仕様がどうなっているのか、上記の観点で確認する必要がありますね。
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'
nonmutating
なdoSomething1(_:)
の隠し引数(self
)はFoo
で、mutating
なdoSomething2(:_)
の隠し引数(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
が渡っています。
というわけで、nonmutating
、mutating
云々というのは、self
の渡し方に関する話でした(そう書いてあったんですが、そういうことでした)。
私は、Ownershipに書いてあるshared
が、最終的にborrowing
になったのかと思って色々と調べ始めました。
上記ドキュメントの書き方だと、inout
がC言語でいうところのT*
を作るのに対して、shared
はconst 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&
)を渡したいというのはよく分かるんですけども。
@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)で呼ばれる
}
@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は以下の通りです。
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
にする方法はまだ分かっていませんので、もう少し調べます。
@callee_ownedはもう現れないのではないか
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
の話が出ていたのかもよく分かりませんでした。
この長かった旅路も、いったんここで終了にしたいと思います。
今回分かったことはまとめたうえで記事にします。
(こんだけ長いと自分でもまとめないとよく分からない)
この長かった旅路も、いったんここで終了にしたいと思います。
こう書いたんですけども、ふと@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ではないと動きません。
-
と思っていましたが、この投稿をする際に、「Compiler Options」フィールドに「-emit-sil」を付ければ出力できることに気付きました ↩︎
@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行目では、TestData1
とTestData2
だけの引数を渡して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
の違いなんでしょうか。
ちょっと奥が深そうなのでいったん切ります。
なぜ、@callee_owned
はstrong_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でそれぞれTestData
、TestData2
が設定されます。
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
していました。
ついで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
/TestData2
のretain
/release
もなくなっています。
結果として、@callee_owned
のときと異なり、クロージャを抜けた時点で020のオブジェクトの参照カウントは2のままです。
このため2回のrelease
が必要になってしまいます。
というわけで。
@convention(thick)
相当のものは@callee_owned
にせよ@callee_guaranteed
にせよ、過去にはあったけども現在は出力されない、ということではないかと推測します。
SIL.rstに記述されているのは「コンテキストデータを参照カウントで管理する」コードですが、現在のSwiftはもっとシンプルなコードが生成されます。
partial_apply
のような処理(カリー化?)も行われなくなっているような気がします。
もともと「ジェネリックなコードから特定の型ごとにコードが生成されるのを避ける」というのが目的だとしても、それが効いてくる「単一のジェネリックなコードから様々な型のコードが生成される」というケースはほとんどない、というのが現実のように思います。せいぜい1個か2個の型が使われるぐらいで。
いよいよSwift 6の時代が来ますが、何というか、コンカレンシー周りも上記の話と似たような臭いがします。最初は理想を追い求めてしまって制約や実装が重いんだけれども、使う人・ケースが増えノウハウが蓄積されるにつれ、適度な落とし所に落ち着くというような。SE-0401なんかは、まさにそういうものという感じがします。
まとめを書こうとして調べ物をしていたら以下のようなページに行き当たりました。
- クロージャへは参照渡し、キャプチャリストを使えば値渡し
- 関数へは値渡し、inoutを使えば
参照渡しwrite-back
クロージャにstruct
を暗黙で渡すコードは検証していませんでした。検証はclass
だけやっていました。
キャプチャリストも試していませんでした。
その2つはやるとして。
なんでしょう、write-back。
ここのコードのことでしょうか。
とりあえず、以下のコードのSILやアセンブラを見てみます。
func testFunc() {
var value: Int = 7
let r = withUnsafeMutablePointer(to: &value) { p in
p.pointee = 18
return 3.0
}
}
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
を呼んでいます。
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_'
クロージャ側では%1
にUnsafeMutablePointer
(=p
)、%5
=p.pointee
、%6
が生のポインタ、%7
は排他アクセスチェック付きのポインタ、のような感じになっていて、p.pointee
の指す先を直接書き換えているように見えます。
アセンブラも見ます。
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
が何をやっているかですね。
withUnsafeMutablePointer
の実装は以下のあたりでしょうか。
単純に、引数の変数のアドレスを取ってクロージャに渡しているだけですね。
なので、結論としてはwrite backのようなことはしていない、ですね。
ちなみに。
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の代入はメインメモリを直接書き換えているが、value
のprint
はレジスタの7から出力している
このいずれかにより結果としてwrite backのように見えていただけではないかと推測します。
(Swift 3.0.2ならコンパイルできるのかと思って試しましたができませんでした)