SwiftはOptimization Level
によって顕著にパフォーマンスに差が出ます。
参考: Apples to apples, Part II · Jesse Squires
また、Dynamic Dispatch
での呼び出しもオーバーヘッドになります。
参考: Swiftのfinal・private・Whole Module Optimizationを理解しDynamic Dispatchを減らして、パフォーマンスを向上する - Qiita
ドキュメントなど読み解けば「どういう記述をすればどうコンパイルされるか」は大体予測付きますが、やはり実際にその予測通りになっているかは確認しておきたい時があります。
特にロガーなどグローバルに呼び出されるものの場合、そういう確認大事だと思っています。
(この記事もロガーの検証が元々の目的で、そのために調べてまとめています。ロガーについても記事化予定です。)
※「逆コンパイル」ではなく「逆アセンブル」の方が良い表現だと思ったので、タイトル変更しました。
Instrumentsツールは?
パフォーマンス計測としては、Instrumentsツールを使うことが多いと思います。
これはInstrumentsツール
のTime Profiler
の結果ですが、使いこなすとけっこうな情報が見られます。
どこのコードに時間かかっているのかの把握までは良いですが、機械語読み解くの大変ですし、Instrumentsツール
使うまでもなくちょっとした検証コードがどうコンパイルされるか知りたいだけならよりシンプルな方法があります。
また後で紹介するHopperは、機械語を可読性の高いC言語っぽい疑似コードに整形してくれる機能があって、読み解きやすくなります。
まずはコマンドラインでSwiftコードをコンパイルしてみる
適当なSwiftコードを用意します。
func test() {
print("hello")
}
test()
コンパイルします。(-o
は出力ファイル名で、省略可能)
xcrun swiftc -Onone sample.swift -o result
-Onone
は冒頭でも書いたOptimization Level
の指定です。
Xcodeでビルドする場合、デフォルトでは以下のようになっています。
- Debug:
-Onone
- 最適化しない
- Release:
-O
- 最適化する
-
-Ounchecked
もある- 最適化する + 実行時のセーフチェックをしないので最も高速?
- リリースビルドに非推奨(ios - Swift -Ounchecked and Assertions - Stack Overflow)
実行するとこのようになります。
./result
# -> hello
ついでに、他のオプションとしては、-D DEBUG
オプションなど付けると、以下の切り替えが可能です。
#if DEBUG
someCall() // `-D DEBUG`オプション付けた時のみ有効
#endif
Optimization Level
の違いを観察
実行ファイルを逆アセンブルする方法は様々ありますが、Hopperというアプリを使ってみます。
とりあえず、最適化あり無し版をそれぞれビルドしておきます。
# 最適化無し
xcrun swiftc -Onone sample.swift -o none
# 最適化あり
xcrun swiftc -O sample.swift -o optimized
# Ounchecked
xcrun swiftc -Ounchecked sample.swift -o unchecked
先日書いたSwiftのfinal・private・Whole Module Optimizationを理解しDynamic Dispatchを減らして、パフォーマンスを向上する - Qiitaの検証のために、以下のコードを用意しました。
最適化ありでも、多分Dynamic Dispatch
になってしまうのでは? と予想してましたが…、
public class Mono {
public func hello() {
println("hello")
}
}
public class Hoge: Mono {
override public func hello() {
println("hoge")
}
}
let m = Mono()
m.hello()
let h: Mono = Hoge()
h.hello()
Hopperというアプリを開いて、D&Dで実行ファイルを読み込むと逆コンパイルしてくれます。
Show Pseudo Code
の機能を使うと、C言語ぽい疑似コードが見られます。
というわけで、先ほどのコードの逆アセンブル結果を見てみます。
-O
(最適化あり)の結果
予想に反して、直接呼び出しになっているように見えます…。
元々、上のSwiftコードはもっとシンプルな作りにしてあったのを、Dynamic Dispatch
発生されるように少し冗長にしたのですが。
同じファイルに記述しちゃったせいですかね(´・ω・`)
原因分かった方は教えてください(´・ω・`)
// _main
int _main(int arg0, int arg1, int arg2) {
swift_once(_globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token4, _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func4, 0x0);
*(int32_t *)__TZvOSs7Process5_argcVSs5Int32 = arg0;
swift_once(_globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5, _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func5, 0x0);
*__TZvOSs7Process11_unsafeArgvGVSs20UnsafeMutablePointerGS0_VSs4Int8__ = arg1;
_TFSs7printlnU__FQ_T_("hello", __TMdSS + 0x8);
_TFSs7printlnU__FQ_T_("hoge", __TMdSS + 0x8);
return 0x0;
}
-Ounchecked
の時もほぼ同じ結果になりました。
-Onone
(最適化なし)の結果
こっちはかなりの量の実行コードが生成されていて、追うのが大変でした(´・ω・`)
生成される関数名の命名規則は、「モジュール名
+ 関数名
+ それらの名前の文字数 + その他」という構成になっているようです。「それらの名前の文字数」は今回は4ばかりですね。
参考: mikeash.com: Friday Q&A 2014-08-15: Swift Name Mangling
// _main
int _main(int arg0, int arg1, int arg2) {
swift_once(_globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token4, _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func4, 0x0, _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func4);
*(int32_t *)__TZvOSs7Process5_argcVSs5Int32 = arg0;
swift_once(_globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5, _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func5, 0x0, __TZvOSs7Process5_argcVSs5Int32, arg0);
*__TZvOSs7Process11_unsafeArgvGVSs20UnsafeMutablePointerGS0_VSs4Int8__ = arg1;
__TF4none4testFT_T_();
return 0x0;
}
// __TF4none4testFT_T_
int __TF4none4testFT_T_() {
rax = __TMaC4none4Mono();
rax = __TFC4none4MonoCfMS0_FT_S0_(rax);
var_8 = rax;
swift_retain_noresult(rax);
(*(*var_8 + 0x48))(var_8);
rax = __TMaC4none4Hoge();
rax = __TFC4none4HogeCfMS0_FT_S0_(rax);
var_18 = rax;
swift_retain_noresult(rax);
(*(*var_18 + 0x48))(var_18);
swift_release(var_18);
rax = swift_release(var_8);
return rax;
}
// __TMaC4none4Mono
int __TMaC4none4Mono() {
rax = *__TMLC4none4Mono;
var_8 = rax;
if (rax == 0x0) {
rax = swift_getInitializedObjCClass(objc_class__TtC4none4Mono);
*__TMLC4none4Mono = rax;
var_8 = rax;
}
rax = var_8;
return rax;
}
// 他にも色々...
// __TFC4none4Mono5hellofS0_FT_T_
int __TFC4none4Mono5hellofS0_FT_T_(int arg0) {
rax = _TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS("hello", 0x5, 0x1, 0x5);
rdi = var_18;
_TFSs7printlnU__FQ_T_(rdi, __TMdSS + 0x8); // やっとprint
rax = swift_release(arg0);
return rax;
}
終わり
というわけで、Swiftのfinal・private・Whole Module Optimizationを理解しDynamic Dispatchを減らして、パフォーマンスを向上する - Qiitaの検証自体は思ったようにうまくいかなかったですが、どうコンパイルされるかが探れるようになって良かったです。
今後も気になった時に確認してみたりしようと思います。
参考資料
-
Secret of Swift Performance — Swift Programming — Medium
- こちらを大いに参考にいたしました。