LoginSignup
170
164

More than 5 years have passed since last update.

Swift実行ファイルを逆アセンブルして、最適化具合を正確に把握する方法

Last updated at Posted at 2015-07-20

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の結果ですが、使いこなすとけっこうな情報が見られます。

Screen Shot 2015-07-20 at 8.02.24 AM.png

どこのコードに時間かかっているのかの把握までは良いですが、機械語読み解くの大変ですし、Instrumentsツール使うまでもなくちょっとした検証コードがどうコンパイルされるか知りたいだけならよりシンプルな方法があります。

また後で紹介するHopperは、機械語を可読性の高いC言語っぽい疑似コードに整形してくれる機能があって、読み解きやすくなります。

まずはコマンドラインでSwiftコードをコンパイルしてみる

適当なSwiftコードを用意します。

sample.swift
func test() {
    print("hello")
}

test()

コンパイルします。(-oは出力ファイル名で、省略可能)

xcrun swiftc -Onone sample.swift -o result

-Ononeは冒頭でも書いたOptimization Levelの指定です。
Xcodeでビルドする場合、デフォルトでは以下のようになっています。

実行するとこのようになります。

./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になってしまうのでは? と予想してましたが…、

sample.swift
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で実行ファイルを読み込むと逆コンパイルしてくれます。

Screen_Shot_2015-07-20_at_9_29_20_AM.jpg

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の検証自体は思ったようにうまくいかなかったですが、どうコンパイルされるかが探れるようになって良かったです。

今後も気になった時に確認してみたりしようと思います。

参考資料

170
164
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
170
164