こんにちは。iOSエンジニアの@kitasukeです。
「SwiftをSILで読んでみる」第2回は「deferをSILで読んでみる」を紹介します。 まだ理解不足なところもあるので、間違いがあればご指摘頂けると嬉しいです。 他の回も興味がある方は、是非こちらをご覧ください。 https://medium.com/swift-in-sil-jp
まずはdefer
のおさらいをします。
defer
とは、スコープを抜けた際に実行する処理を記述できます。スコープ内のどの場所にdefer
を定義しても、実行されるタイミングは一番最後になります。
実際の用途としては、下記のようにスコープを抜ける前に忘れずに実行したい処理を書くケースが多いと思います。
let db = DB()
defer { db.close() }
let user = db.getUser()
...
let data = Object()
defer { data.dealloc }
let user = data.user
...
今回の記事では、一体defer
はどのように実行順を保証しているのかSILから読み取ってみましょう。
さっそくdefer
がSILでどのように表現されているかを見てみましょう。
今回は単純にInt
を返すメソッドを例として使用します。ローカル変数宣言直後にdefer
内で値を代入しています。
期待される挙動としては、このメソッドの返り値はdefer
内で代入されている10ではなく、0となることです。
defer.swift
func number() -> Int {
var x: Int
defer { x = 10 }
x = 0
return x
}
さて、swiftcコマンドでcanonical SILを出力してみましょう。出力された結果の全てが関連しているわけではないので、分からない箇所があっても特に問題ないです。
$swiftc -emit-sil defer.swift -o defer.sil
// number()
sil hidden @_T05defer6numberSiyF : $@convention(thin) () -> Int {
bb0:
%0 = alloc_stack $Int, var, name "x" // users: %9, %6, %3, %10
%1 = integer_literal $Builtin.Int64, 0 // user: %2
%2 = struct $Int (%1 : $Builtin.Int64) // users: %11, %4
%3 = begin_access [modify] [static] %0 : $*Int // users: %4, %5
store %2 to %3 : $*Int // id: %4
end_access %3 : $*Int // id: %5
%6 = begin_access [read] [static] %0 : $*Int // user: %7
end_access %6 : $*Int // id: %7
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
dealloc_stack %0 : $*Int // id: %10
return %2 : $Int // id: %11
} // end sil function '_T05defer6numberSiyF'
// $defer #1 () in number()
sil private @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () {
// %0 // users: %4, %1
bb0(%0 : $*Int):
debug_value_addr %0 : $*Int, var, name "x", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 10 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %5
%4 = begin_access [modify] [static] %0 : $*Int // users: %5, %6
store %3 to %4 : $*Int // id: %5
end_access %4 : $*Int // id: %6
%7 = tuple () // user: %8
return %7 : $() // id: %8
} // end sil function '_T05defer6numberSiyF6$deferL_yyF'
まずメソッド定義が2つ出力されていることが分かります。先程定義したnumber
と、それとは別にdefer
も出力されています。そして良く読んでみると、number
メソッド内のコメントアウトに$defer #1 () in number()
と書いてある部分があります。この行がdefer
内で定義した処理が実行されているように見えます。
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
function_ref
とはメソッドの参照を作る命令で、apply
は引数を渡してfunction_ref
で参照している制御を適用します。つまり、number
メソッドの下に出力されているdefer
メソッドが実行されます。
// $defer #1 () in number()
sil private @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () {
// %0 // users: %4, %1
bb0(%0 : $*Int):
debug_value_addr %0 : $*Int, var, name "x", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 10 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %5
%4 = begin_access [modify] [static] %0 : $*Int // users: %5, %6
store %3 to %4 : $*Int // id: %5
end_access %4 : $*Int // id: %6
%7 = tuple () // user: %8
return %7 : $() // id: %8
} // end sil function '_T05defer6numberSiyF6$deferL_yyF'
defer.swiftで定義した通りに、変数x
に10を代入しています。ここで空のタプルを返しているのは本題から逸れるので無視します。
ただ一つ疑問が残ります。
今回の出力されたcanonical SILの順番だと、number
メソッドでreturn
をする前にdefer
の命令が実行されているので、defer
内で代入した10が返り値になってしまうのではないでしょうか?
出力されたcanonical SILを読み直す前に、SSA形式(静的単一代入形式)の説明を簡単にします。SSA形式は各変数に値が一度しか代入されないようになっています。SSA形式はLLVMの中間表現にも使われていて、Swiftコンパイラが生成するSILにも同様に使われています。
例えば、var
に値を複数回代入するコードは下記のように表現されます。
var x = 0
x = 5
x = 10
x0 = 0
x1 = 5
x2 = 10
このように、SSA形式を用いて各変数に一時的な値も保持することで、コンパイラの最適化を非常に効率よく実行できる利点があります。今回のdefer
の例も、実はSSA形式を利用して命令が実行されています。
それではcanonical SILを再度見てみましょう。
// number()
sil hidden @_T05defer6numberSiyF : $@convention(thin) () -> Int {
bb0:
%0 = alloc_stack $Int, var, name "x" // users: %9, %6, %3, %10
%1 = integer_literal $Builtin.Int64, 0 // user: %2
%2 = struct $Int (%1 : $Builtin.Int64) // users: %11, %4
%3 = begin_access [modify] [static] %0 : $*Int // users: %4, %5
store %2 to %3 : $*Int // id: %4
end_access %3 : $*Int // id: %5
%6 = begin_access [read] [static] %0 : $*Int // user: %7
end_access %6 : $*Int // id: %7
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
dealloc_stack %0 : $*Int // id: %10
return %2 : $Int // id: %11
} // end sil function '_T05defer6numberSiyF'
まず最初に%0
には宣言した変数が定義されています。その次に、%2
がinteger_literal
の0をstruct
として定義されています。よく見てみると、defer
を実行するためのapply %8(%0)
には引数として%0
を渡しています。そしてreturn
ではreturn %2
と書いてあるように%2
がメソッドの返り値として使われていて、defer
内で実行されたものは特に使われていません。上記のことから、defer
がスコープを抜けた際に記述された処理の実行を保証していることが分かりました。
今回はdefer
をSILで読んでみました。SILでどのように表現されているかを見て、defer
の仕組みを理解できました。また今回深くは説明していませんが、SSA形式というSILの基本的な構造も知れました。これ以外にもコントロールフロー・データフローなど色々な仕組みを用いてコンパイラは最適化を行っているので、興味がある方は調べてみると良いと思います。皆さんもSwiftで気になることがあればSILを読んでみましょう。