WWDC Quick Look 💓 By SwiftGGTeam
Link fast: Improve build and launch times

Link fast: Improve build and launch times

元の動画を見る

ハイライト

Apple は、ld64 静的リンカーの速度を 2 倍にし、dyld に連鎖フィックスアップとページイン リンク技術を導入して、アプリの構築、起動、サイズの縮小を高速化しました。

主要内容

静的リンク: コンパイルから実行可能ファイルまで

作成したコードを実行するには、他のライブラリと連携する必要があります。静的リンカー (ld) は、複数の .o ファイルと静的ライブラリを実行可能ファイルにマージする役割を果たします。静的ライブラリ (.a) は基本的に次のものを使用します。arパッケージ化された .o ファイルのコレクション。リンカーは、未定義のシンボルを解決できる .o ファイルのみをロードします。

この選択的読み込みメカニズムは賢いですが、問題も引き起こします。リンカーは静的ライブラリを固定された順序でシリアルに処理する必要があり、マルチコアを完全に活用することができません。

01:57

ld64 は今年 2 倍高速です

Apple の静的リンカー ld64 は今年大幅に最適化されました。コンテンツのコピー、LINKEDIT の構築、UUID の計算、およびコード署名ハッシュを並列化することにより、リンク速度が 2 倍になります。

07:06

ダイナミック リンク: 起動時の隠れたコスト

ダイナミック ライブラリ (dylib) は、ビルド時にシンボル参照とライブラリ パスのみを記録し、コードはコピーしません。これによりアプリのサイズは縮小されますが、コストは起動時に延期されます。起動時に dyld をロード、マッピング、シンボル解決し、修正を実行する必要があります。

15:47

連鎖的な修正: バイナリが小さくなり、起動が速くなります

今年、Apple は連鎖フィックスアップ形式を導入しました。従来のフィックスアップでは、LINKEDIT 内の各フィックスアップ位置を記録する必要がありますが、チェーン フィックスアップでは、LINKEDIT 内の各ページの最初のフィックスアップ位置のみが記録され、残りの情報は DATA セグメント自体のポインターにエンコードされます。これにより、LINKEDIT のサイズが大幅に削減されます。

22:37

ページイン リンク: カーネルが修正を行います

連鎖修正とともに、dyld はページイン リンクを導入します。カーネルは、起動時にすべての dylib に対して一度にフィックスアップを実行するのではなく、DATA ページが最初にアクセスされたときにフィックスアップを遅延して適用します。これにより、起動時間とダーティ メモリが削減され、DATA_CONST ページはリサイクルおよび再構築できるクリーンなページになります。

25:07

詳細

静的ライブラリの選択的読み込みメカニズム

// main.c
int main() {
    foo();
    return 0;
}

// foo.c
void foo() {
    bar();
}

// bar.c
void bar() {
    // implementation
}
void unused_func() {
    // never called
}

// baz.c
void baz() {
    undef(); // undefined symbol
}

コンパイル後、main.o、foo.o、bar.o、baz.o が得られます。 bar.o と baz.o を静的ライブラリ lib.a にパッケージ化します。

リンクコマンド:

ld main.o foo.o -l.a -o app

キーポイント:

  • リンカはコマンドラインの順序でファイルを処理します。
  • 最初に main.o をロードし、main が定義され、foo が未定義であることを確認します。
  • foo.o を再度ロードして foo を解析しますが、新しいバーは未定義です
  • 静的ライブラリ lib.a をチェックし、bar.o が bar を定義していることを確認し、bar.o をロードします。
  • baz.o は lib.a にもありますが、それを必要とする未定義のシンボルがないため、ロードされません。
  • これは静的ライブラリの選択的なロードであり、未使用の .o ファイルは最終プログラムには入りません。

静的リンクを高速化する 4 つのオプション

1. -all_load: すべての静的ライブラリのコンテンツを並行してロードします

# Other Linker Flags
-all_load

該当するシナリオ: アプリは最終的にほとんどのコンテンツを静的ライブラリに読み込みます。このオプションを使用すると、リンカーはすべての静的ライブラリを並列で解析し、シリアル制限を回避します。ただし、複数の静的ライブラリが同じシンボルを実装すると、競合が発生します。

2. -dead_strip: 未使用のコードを削除します

# Build Settings -> Dead Code Stripping -> YES
# 或在 Other Linker Flags 中添加
-dead_strip

協力する-all_loadすべてのコンテンツをロードする際のサイズの増加を補うために使用されます。リンカーは、メインから到達できないコードとデータを削除します。

3. -no_exported_symbols: エクスポートされたシンボル テーブルの構築をスキップします

# Other Linker Flags
-no_exported_symbols

通常、メイン アプリではシンボルをエクスポートする必要はありません。エクスポート トライ ビルドをスキップすると、リンク時間を数秒節約できます。まず、次のコマンドを使用して、エクスポートされたシンボルの数を確認します。

dyld_info -exports /path/to/YourApp

該当しないシナリオ: アプリがメインの実行可能ファイルにリンクされたプラグインを読み込むか、XCTest を使用してアプリをホストとしてテストを実行します。

4. -no_deduplicate: デバッグ ビルドは重複排除をスキップします

# Other Linker Flags
-no_deduplicate

C++ テンプレートを拡張すると、同じ命令を持つ多数の関数が生成され、リンカーは重複を削除してサイズを削減します。ただし、重複排除アルゴリズムには時間がかかります。 Xcode 14 は、デフォルトでこのオプションをデバッグ構成に渡します。カスタム ビルド システムを使用する場合は、デバッグ ビルドにこのオプションが追加されていることを確認してください。

ダイナミックリンクの修正メカニズム

App TEXT 段:
  call _malloc    →  实际变成:
  
  // stub 代码(TEXT 段)
  ldr x16, [DATA, #malloc_ptr]
  br x16

App DATA 段:
  malloc_ptr: 0x0  ← dyld 启动时填入 libSystem 中 malloc 的实际地址

キーポイント:

  • TEXT セグメントは不変です (コード署名要件)
  • 外部 dylib 関数を呼び出すと、リンカーはスタブ コードを生成します
  • スタブは DATA セグメントから関数ポインタをロードしてジャンプします。
  • dyld が開始されると、DATA セグメントのみが変更され、シンボルのバインドが完了します。
  • すべての修正は基本的に DATA セグメント内のポインターを dyld 設定します。

連鎖フィックスアップ形式

従来のフィックスアップ: LINKEDIT は、各フィックスアップのアドレスの場所を記録します。

Chained fixups:

  • LINKEDIT は、各ページの DATA の最初の固定位置のみを記録します。
  • 後続のフィックスアップは、ポインタのオフセット ビットを介して「連鎖」されます。
  • ポインタはパックされています: バインド、次のフィックスアップのオフセット、シンボル インデックス、内部オフセットのいずれであっても

必要とする:

  • 導入対象 iOS 13.4以降
  • Xcode 14 は修飾されたターゲットに対して自動的に生成します

ページインリンクのワークフロー

启动流程(优化前):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. 对所有 dylib 执行 fixup  ← 耗时
5. 运行 initializers

启动流程(优化后):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. fixup 由内核在 page-in 时惰性执行  ← 省时间
5. 运行 initializers

キーポイント:

  • 初回起動時の dyld キャッシュグリーンステップジョブ
  • 再利用キャッシュのその後の起動
  • ページインリンクにより、修正ステップがさらに最適化されます。
  • 起動時にロードされた dylib でのみ動作します。dlopendylib は依然として伝統的に dyld によって処理されます。

診断ツール

dyld_usage: dyld の起動動作を追跡します

dyld_usage -t /Applications/TextEdit.app/Contents/MacOS/TextEdit

出力例:

launch time: 15ms
fixups: 1ms   ← page-in linking 的贡献
static initializers: 12ms  ← 现在是大头

dyld_info: バイナリ ファイルをチェックします

# 查看 fixup 信息
dyld_info -fixups /path/to/binary

# 查看导出符号
dyld_info -exports /System/Library/Frameworks/Foundation.framework/Foundation

# 查看 dyld cache 中的库
dyld_info -exports Foundation

重要ポイント

  • 静的ライブラリと動的ライブラリのバランス ポイントを評価する: 今年は ld64 が 2 倍高速になりました。より多くの静的ライブラリまたはソース ファイルをアプリに直接コンパイルする余裕があり、dylib の数を減らして起動コストを削減できます。使用dyld_usage起動時間を測定し、プロジェクトに適したバランスを見つけてください。

  • デバッグ ビルドが有効 -no_deduplicate: C++ またはカスタム ビルド システムを使用している場合は、デバッグ構成が追加されていることを確認してください。-no_deduplicateこれにより、リリース ボリュームの最適化に影響を与えることなく、リンク時間を大幅に短縮できます。

  • 連鎖フィックスアップをチェックして有効にする: デプロイメント ターゲットを iOS 13.4 以降にアップグレードすると、Xcode 14 は連鎖フィックスアップ形式を自動的に生成します。使用dyld_info -fixupsiOS 16 でのページイン リンクの起動速度を取得するために、バイナリがすでに新しい形式を使用していることを確認します。

  • 大規模なアプリ リンクを高速化するには、-no_exported_symbols を使用します: メインのアプリ ターゲットに追加してみてください-no_exported_symbols、最初に使用しますdyld_info -exportsエクスポートされたシンボルの数を確認します。数が大きい場合 (例: 数十万)、このオプションによりリンク時間を数秒節約できます。

  • 静的イニシャライザの作業を削減:dyld_usage静的イニシャライザの消費時間が表示されます。コード内のグローバル変数の初期化ロジックを確認し、イニシャライザでの I/O、ネットワーク リクエスト、または複雑な計算の実行を避けてください。

関連セッション

コメント

GitHub Issues · utterances