Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Created May 26, 2018 06:54
Show Gist options
  • Save kitasuke/bcbac778dc9582c5f6a47a78e389bb23 to your computer and use it in GitHub Desktop.
Save kitasuke/bcbac778dc9582c5f6a47a78e389bb23 to your computer and use it in GitHub Desktop.
defer-sil-jp

deferをSILで読んでみる

こんにちは。iOSエンジニアの@kitasukeです。

「SwiftをSILで読んでみる」第2回は「deferをSILで読んでみる」を紹介します。 まだ理解不足なところもあるので、間違いがあればご指摘頂けると嬉しいです。 他の回も興味がある方は、是非こちらをご覧ください。 https://medium.com/swift-in-sil-jp

defer

まずはdeferのおさらいをします。
deferとは、スコープを抜けた際に実行する処理を記述できます。スコープ内のどの場所にdeferを定義しても、実行されるタイミングは一番最後になります。

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Statements.html#//apple_ref/doc/uid/TP40014097-CH33-ID532

実際の用途としては、下記のようにスコープを抜ける前に忘れずに実行したい処理を書くケースが多いと思います。

let db = DB()
defer { db.close() }

let user = db.getUser()
...
let data = Object()
defer { data.dealloc }

let user = data.user
...

今回の記事では、一体deferはどのように実行順を保証しているのかSILから読み取ってみましょう。

SIL

さっそくdeferがSILでどのように表現されているかを見てみましょう。

使用するメソッド

今回は単純にIntを返すメソッドを例として使用します。ローカル変数宣言直後にdefer内で値を代入しています。
期待される挙動としては、このメソッドの返り値はdefer内で代入されている10ではなく、0となることです。

defer.swift

func number() -> Int {
    var x: Int
    defer { x = 10 }
    x = 0
    return x
}

canonical SIL

さて、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が返り値になってしまうのではないでしょうか?

SSA形式

出力された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には宣言した変数が定義されています。その次に、%2integer_literalの0をstructとして定義されています。よく見てみると、deferを実行するためのapply %8(%0)には引数として%0を渡しています。そしてreturnではreturn %2と書いてあるように%2がメソッドの返り値として使われていて、defer内で実行されたものは特に使われていません。上記のことから、deferがスコープを抜けた際に記述された処理の実行を保証していることが分かりました。

まとめ

今回はdeferをSILで読んでみました。SILでどのように表現されているかを見て、deferの仕組みを理解できました。また今回深くは説明していませんが、SSA形式というSILの基本的な構造も知れました。これ以外にもコントロールフロー・データフローなど色々な仕組みを用いてコンパイラは最適化を行っているので、興味がある方は調べてみると良いと思います。皆さんもSwiftで気になることがあればSILを読んでみましょう。

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment