Swiftの中間言語SILを読む その1 - SILに入門するための準備

January 9, 2018

年末年始はSemaにおけるGenerics周りの実装を読むためにImplementing Swift Genericsの動画をずーーっと見ていた。この動画はLLVMのカンファレンスでの発表なのでSILにはほぼ言及せずにLLVM IRの動きに近い擬似コードでSwiftのGenericsの実装の解説がされているのだが、その過程をみるとSILの生成や最適化の段階でどんなことをしているのかが見えてきて、結果楽しくなってSIL/SILGen/SILOptimizerあたりのコードリーディングを少しだけ始めてしまった。

SemaでGenericsを読む場合、ASTモジュールのGenericsContext / GenericSignature / GenericEnvironment などのクラスを読む必要があるが、これらのクラスはSILGen以降のフェーズ向けの実装が多く入っているので、SILについて理解する → この辺りのクラスが読めるようになる → SemaでのGenericsも読めるという流れで結果的に効率よく進められている気がする。もっと早くSemaに固執せずいろんなモジュールを薄く広く触ってみた方がよかったのかもしれない。。

今回は最初なので概念・用語〜デバッグ方法あたりをまとめてみる。

SIL (Swift Intermediate Language)

Swiftコンパイラでは直接LLVM IRに落とし込まずに一旦SIL(Swift Intermediate Language)という中間表現を経由する。主に最適化をしたり、フローに基づいた解析(例えば、変数が初期化されているかどうかチェックする、など)を行う。

img

余談だが、逆に言うと型チェックの段階では変数が初期化されているかどうかはチェックしない。これを利用して型システムのテストでは初期化なしで型のチェックのみを行なっているテストがあったりする。

var pqt: (P & Q).Type
pt = pqt
qt = pqt

2種類のSILと2種類のOptimization

SILには最適化のされ具合で区別された2種類のSILraw SILcanonical SILがある。

raw SILは型チェック後にSILGenモジュールによってASTから変換されたばかりの状態のSILを表す。

そこからSILOptimizerモジュールによって最適化がされるわけだが、最適化には大きく分けて、Guaranteed OptimizationGeneral Optimization の2つがある。

Guaranteedな最適化は、たとえ-Ononeオプションをつけたとしても必ず実行される最適化で、言語モデルの一部と言えそう。具体的にどんなものがあるかは今後見ていく予定。SILのドキュメントに書いてあるのでいまはそちらを参照。

一方でGeneralな最適化は-Oで最適化を有効にした場合にのみ実行される。主にパフォーマンス関連の最適化で、ここに含まれるものとして一番特徴的なのがGenericsのSpecialization。C++やRustのGenericsと違ってSpecializeはあくまでも最適化の1つであり、SwiftにおけるGenericsの実装方式はvalue witness tableやprotocol witness tableを使ったものであることに注意。こちらも他にどんなものがあるかはドキュメントを参照。

raw SIL / canonical SILの話題に戻すと、raw SILに対してGuaranteed Optimizationが施された状態がcanonical SILである。 canonical SILをさらに最適化するのがGeneral Optimizationである。

IMG_0144.jpg (76.5 kB)

デバッグ方法

基本は-emit-sil-emit-silgenを使えば良い。

  • -emit-silgenはraw silを出力する
  • -emit-silは最適化もされたcanonical silを出力する
$ swiftc -emit-silgen test.swift
$ swiftc -emit-sil test.swift

デフォルトではGeneral Optimizationはオフ(つまり-Ononeと同じ)みたいなので、Guaranteed OptimizationだけでなくGeneral Optimizationも有効にしたい場合は-O オプションをつける。

$ swiftc -O -emit-sil test.swift

また、最適化の各過程をすべて出力する-sil-print-allという便利なコマンドもある。これも-Oと組み合わせて使う。

$ swiftc -Xllvm -sil-print-all test.swift
$ swiftc -O -Xllvm -sil-print-all test.swift
*** SIL module before Guaranteed Passes transformation (0) ***
// hoge()
sil hidden @_T05test24hogeSiyF : [email protected](thin) () -> Int {
bb0:
  // function_ref Int.init(_builtinIntegerLiteral:)
  %0 = function_ref @_T0S2iBi2048_22_builtinIntegerLiteral_tcfC : [email protected](method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %3
  %1 = metatype [email protected] Int.Type                   // user: %3
  %2 = integer_literal $Builtin.Int2048, 1        // user: %3
  %3 = apply %0(%2, %1) : [email protected](method) (Builtin.Int2048, @thin Int.Type) -> Int // users: %5, %4
  debug_value %3 : $Int, let, name "x"            // id: %4
  return %3 : $Int                                // id: %5
} // end sil function '_T05test24hogeSiyF'

*** SIL module after Guaranteed Passes "Capture Promotion to Eliminate Escaping Boxes" (1) ***
// hoge()
sil hidden @_T05test24hogeSiyF : [email protected](thin) () -> Int {
bb0:
  // function_ref Int.init(_builtinIntegerLiteral:)
  %0 = function_ref @_T0S2iBi2048_22_builtinIntegerLiteral_tcfC : [email protected](method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %3
  %1 = metatype [email protected] Int.Type                   // user: %3
  %2 = integer_literal $Builtin.Int2048, 1        // user: %3
  %3 = apply %0(%2, %1) : [email protected](method) (Builtin.Int2048, @thin Int.Type) -> Int // users: %5, %4
  debug_value %3 : $Int, let, name "x"            // id: %4
  return %3 : $Int                                // id: %5
} // end sil function '_T05test24hogeSiyF'

... 

また-sil-print-only-functions-sil-print-only-functionなどのコマンドを使って、特定の関数についてのみを出力するようにすることも可能。-sil-print-only-functionsは渡された名前を含む関数を出力する。-sil-print-only-functionは渡された名前に完全一致する関数のみを出力する。

いずれも-sil-print-allと組み合わせて使う。

$ swiftc -O -Xllvm -sil-print-all -Xllvm -sil-print-only-functions=hoge test.swift

SILの時点ですでに関数名がマングルされていることに注意。つまりhogeという関数は_T05test4hogeSiyF みたいな文字列になっているので、一度-emit-sil等でどんな名前かを確認したほうがよさそう。

それ以外にもオプションがいくつかあるので、DebuggingTheCompiler.rstPassManager.cppを参考。

特定の最適化を有効にするとか、どんな最適化があるか一覧表示とかのオプションがあればよかったのだけどたぶんなさそう一応ありました。その4 - sil_optコマンドの使い方を参考。ただし使いづらいので-sil-print-allを使う方がよさそう。

SILに入門するための参考文献

あとは最初に貼った動画は直接的にはSILはでてこないけど、witness tableなどまさにSILがやっている部分の概念の解説がされているのでおすすめ。

最適化はオレオレなものはほとんどなく、基本的にはコンパイラの最適化理論に基づいたものなので、一般的なコンパイラの本(ドラゴンブックやタイガーブック)が手元にあるとなお良いかもしれない。

まとめ

とりあえず入門のための最低限の事項をまとめてみた。 SIL関連のモジュールは(特にSILOptimizerが)コード量が多いものの、依存関係が綺麗で元になった理論も見つけやすいのでSemaに比べて圧倒的に読みやすいかもしれない。。

型システムも引き続き読みつつ、時間を見つけてこのシリーズも書いていく。

このエントリーをはてなブックマークに追加