ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Xcodeビルドでの並列化に関する解説
Xcodeのビルドシステムより、ご利用のビルドから最大の並列処理が抽出される仕組みを解説します。さらに、ビルド効率が向上するプロジェクトの構築方法、Xcodeにおけるターゲットのビルドフェーズ間の関係を解消するプロセス、Swiftでコンパイルする際に利用可能なハードウェアリソースを最大限に活用する方法について解説します。また、ビルドの効率性やパフォーマンスを監視する際に強力なツールとなるBuild Timelineも紹介します。
リソース
関連ビデオ
WWDC22
-
ダウンロード
♪ ♪ こんにちはWWDC2022へようこそ Xcode build system teamの エンジニアのBenです Swift Cpmpiler teamの エンジニアのArtemです この講演では Xcodeのビルド プロセスを深く掘り下げます ビルドにおける並列化の謎を解き明かします Benはビルドに関する概念の紹介から始めて ビルドのパフォーマンス問題を調査するために Xcodeが提供する利用可能な ツールについて見ていきます Xcodeが どのように並列化を 進めるかを説明します その上に多くのターゲットで 構成されるプロジェクトを 構築しながら Xcodeがどのようにビルドに 全体的に並列化するかを説明し 最後にポイントをまとめます Ben? XcodeでCMD+Bを押してAppをビルドすると 何が起こるのか説明します Xcodeの一部であるビルドシステムは すべてのソースファイル アセット ビルド設定および実行先などの設定を含む プロジェクト全体の表現で呼び出されます ビルドシステムは唯一の真実の情報源です どのツールを呼び出すかどの設定を使うか 中間ファイルを作成するか 最終的にAppを作成します 次のステップではビルドシステムが プロジェクトの入力ファイルを処理するツール 例えばコンパイラを呼び出します
ClangとSwiftの両方のコンパイラは Appを表す実行プログラムをリンクするために リンカが必要とする オブジェクトファイルを生成します この順番は理にかなっているのですが どこからくるのかは明らかではありません では そのプロセスの一例を見てみましょう ビルドシステムがどのような順序で 実行するか決定します
入力されたソースファイルを使って Swiftコンパイラは プログラマの意図を理解し 実行可能なバイナリにします ソースコードのチェックをチェックします この処理は失敗する可能性があります 成功すると 各入力に対して オブジェクトファイルを作成し これらのオブジェクトファイルは リンカーを呼び出すために使用され リンカーはオブジェクトファイルを結合し 外部リンクされたライブラリへの参照を追加して 実行ファイルを生成します この2つのタスクは 消費と生成の依存関係にあります コンパイラが生成し リンカーで消費されます これはビルドシステムグラフの 依存関係を作成します ビルドシステムは ファイルの内容には興味がありませんが タスク間の依存関係は必要です ビルドの実行中に 他のタスクの入力を生成するタスクが そのタスクが開始する前に 終了することを確認する必要があります そしてこの核となる概念は あらゆる種類のタスクに有効であるため タスクAとタスクBの依存関係を示す より一般的な可視化に切り替えてみます この場合 AはBの入力の一部 または全部を生産しています
コンパイルとリンクは ターゲットを構築するために実行される 多くのタスクのほんの一部に過ぎません ファイルのコピーや コードサイニングなどもあります フレームワークの目標を 構築することを表しています 繰り返しになりますがこれらのタスクには 入力と出力に基づく 依存関係が定義されています つまり実行中のタスクAが終了すると 実行中のタスクBとCのブロックが解除され タスクBが終了すると タスクDとEのブロックが解除されるのです ブロック解除のタスクを ダウンストリームと呼び ブロックされるタスクを アップストリームと呼びます 多くのプロジェクトは複数のフレームワーク ターゲットを含んでいるので AppとExtensionを表すターゲットを 2つ追加してみましょう ターゲットは 明示的または 暗黙的な依存関係によって プロジェクト内の相互の依存関係を定義します 例えば「 バイナリをライブラリに リンクする」ビルドフェーズの追加です
この場合 AppはExtensionを埋め込み フレームワークに対してリンクします Extensionはフレームワークを 使用していないので 依存関係はありません
ビルドグラフを実行するとき 異なるタスクは異なる時間がかかります 作品を完成させるために必要な 複雑さのレベルの違いです 必要な計算や入力の大きさによって異なります 通常多くのファイルをコンパイルすることは 数個のヘッダーのファイルを コピーするより時間を要します ビルドシステムはこのビルドを実行するとき 依存関係のないタスクの実行から始めます 完了すると下流のタスクの ブロックを解除します 予定されているすべてのタスクが終了するまで
ビルドシステムは入力が 変更されていないタスクを スキップできます 出力は最新のままです この場合 AppのターゲットのBのように 入力が変わると再実行が必要なタスクは 出力が変われば 下流のタスクも再実行しなければなりません 他のタスクをすべてのスキップすることで プロジェクトの反復作業時に 非常に速い応答時間を実現します しかし 今はフルビルドにこだわってみましょう
タスク実行の依存関係や継続時間は 下流タスクが開始できる 最初の時間を定義します この情報を元にクリティカルパスを計算します ビルドの実行に必要な最短の時間は 理論上は無限の資源を持ちます 本講演では このパスを短縮し 並列性と拡張性の高い ビルドグラフを作成することを 共通のテーマとします クリティカルパスの短縮は 必ずしも全体のビルド時間の短縮に つながるわけではありませんが ハードウェアに合わせてスケールを保証します クリティカルビルドパスは ビルドの速度を制限するもので たとえハードウェアが可能にしても より速く完成させることはできません クリティカルパスの短縮は 依存関係を断ち切ることで実現します ビルドがどのように実行されたかを見て 実行された時間に基づいて データのプロットが必要です 幅はタスクの長さを表しています この2つの幅広い要素は 長く続くタスクを示します このように幅の狭い要素は 早く終わるタスクを表します
グラフの高さはある時刻に 並列実行されているタスクの数を示しています CPUやメモリの使用率に 直接対応するものではありません
この2つのシナリオのように タスクが下流のタスクを ブロックすることによって 空きが発生するのです 要素の色は 関連のターゲットを表します Xcode14で新しく導入された ビジュアライゼーションは ビルド終了後のパフォーマンスの理解に 役立ちます Xcode Build Timelineは ビルドログ追加の新機能です 並列度に基づいて可視化されます 階層構造ではなく ビルドのパフォーマンスを把握できます ある時間における行数は 並列度の高さを表します 個々のタスクの水平方向の長さは タスクを終えるために必要な時間です グラフ内の空白は未完成のタスクが 下流タスクの阻害を示しています タイムライン要素に適用される異なる色は ビルドの一部である異なる ターゲットの区別に役立ちます インクリメンタルビルドのタイムラインには 実際に実行されたタスクだけが表示されます インクリメンタルビルドでは タイムラインは実行されたタスクだけを含み 長時間実行されるタスクを発見します
Xcode14のビルドタイムラインの デモを紹介します ウィンドウで ドキュメントコンパイラを ビルドする swift-doccプロジェクトを Githubからコピーして開きました このスキームで構築された ターゲットの把握ができます スキームエディタを確認してみましょう スキームをクリックし “Edit scheme”を選択します
ビルドタブには すべてのターゲットのリストが表示され ターゲットは明示的に スキームに追加することも すでにスキームの一部である ターゲットの依存関係になることで 暗黙的に追加することも可能です 今回はSwift パッケージに自動生成された スキームを使用しているため マニフェストからのターゲットは すべて明示的に定義されています
このログは 先ほど実行した スキームビルドを表しています ビルドシステムが実行した タスクエントリーが含まれます エントリーはこの'docc'ターゲットのように 所属するターゲットに基づいた 階層構造で構成されています そのターゲットの実行ファイルを 正常にビルドするために Xcodeはこのノードの子で表される すべてのタスクを実行しました またインクリメンタルビルドで 再実行する必要のない 以前のビルドのタスクも表示されます ビルドのタスクも表示されます 'Recent'を選択すると 実際に実行されたタスクだけが表示され スキップしたタスクをすべて非表示にします さらにビルドログは フィルターもサポートしています 問題があったあるいは失敗したタスクだけを 表示しています
ビルドタイムラインを開くには エディタオプションから アシスタントを開きます ビルドタイムラインは ビルドログの横に開きます いつものようにエディタオプションで アシスタントを右 または下に表示するか設定できます 今現在のところボトムでいきます タイムラインはビルドの並列化に基づき 「最近の」ビルドログと 同じデータを可視化します 一方の要素を選択すると 他方の要素も選択されます これはタスクの実行を 文脈で見ることができます このタイムラインはタスクについて 選択したタスクと並列に実行されます トラックパッドのピンチジェスチャーで 再度ズームアウトしています
タイムライン上でエレメントを選択すると ビルドログに表示されます またビルドログは階層構造に 基づいて可視化されるので このコンパイラの呼び出しの一部として どのファイルがコンパイルされたかを見れます ビルドタイムラインでオプションを押しながら
領域を選択するとビューポートが調整されます ビューポートを調整をこの時間枠に合わせます 対象となるArgumentParserの リンクが確認できます コンパイルを待っていることがわかります Optionを押しながら上にスクロールすると 素早くズームアウトすることができます タイムラインの行数は 並行して動いていたタスクの数を表します このように空いたスペースは生産されていない インプットを待っているタスクを表しています 垂直方向に埋め尽くされ 空白部分ない状態が理想的です これは ビルドグラフを最もよくスケーリングし 高速であればあるほどビルドを高速化します そのために Xcodeは今年クリティカルパスを 短縮するための多くの改良が施されています 次に Xcodeがどのように 個々のターゲットを定義し ビルドするかを確認してみましょう ターゲットを設定する際 そのターゲットを作るために必要な作業を ビルドフェーズで記述します プロジェクトエディターで定義され コンパイルするソースコードファイルや アセット リソースなど リンクする必要のあるライブラリや 実行する必要のあるスクリプトを含みます 多くのビルドフェーズではタスクの入力や出力 他のビルドフェーズから依存関係を作成します 例えば ターゲットのソースファイルは リンクする前にコンパイルする必要があります ただしすべてのビルドフェーズに 適用されるわけではありません 各ビルドフェーズのタスクを 直線的に実行するのではなく ビルドシステムはビルドフェーズの 入力と出力を考慮し 並行して実行可能かどうかを判断します 例えば コンパイルとリソースのコピーは 並列に実行できます どちらも他方に依存しないからです しかしリンクはコンパイルの後でなくてはなりません オブジェクトファイルに依存しているからです では 別のターゲットで考えてみましょう 他のビルドと異なりスクリプトの入出力は ターゲットエディターで 手動で設定する必要があります 結果 ビルドシステムは連続した スクリプトを実行します 一度の実行でビルド時の データ競合を回避しています ターゲット内のスクリプトが 依存関係分析に基づいて 実行されるように設定されている場合 スクリプトの完全なリストを指定します 指定する場合 ビルド設定 FUSE_BUILD_SCRIPT_PHASESを YESに設定すると ビルドシステムは 並行して実行しようとすることを示します スクリプトのフェーズを並列で実行した場合 指定された入力と出力に 依存する必要があります そのためスクリプトの 入出力リストが不完全な場合 データ競合を引き起こす可能性があります これを緩和するために以下の機能をサポートし 各スクリプトフェーズの 依存関係を正確に宣言します サンドボックスはシェルスクリプトが誤って ソースファイルや中間ビルドに アクセスすることを 明示的に宣言されない限りブロックする オプトイン機能です この例ではinputとoutput.txtのどちらも スクリプトフェーズの 依存関係として宣言されます プロジェクトのビルド時にスクリプトが 両方のファイルへの読み書きを ブロックするようにします スクリプトがサンドボックスに違反した場合 0以外の終了コードで失敗しビルドが失敗します さらにXcodeは宣言されずアクセスしようとした すべてのパスをリストアップします このスクリプトフェーズに依存情報として 両ファイルの追加でこの問題が修正されます スクリプトが誤って宣言された入出力以外の ファイルに誤ってアクセスしないようにします 複数のスクリプトフェーズが ある例を見てみましょう データ競合や不正なビルドを 防ぐ方法を見てみましょう スクリプトフェーズは2つあります テキストファイルを読み込む チェックサムを計算する その値を中間ファイル DERIVED_FILE_DIRに書き込む もう一方のスクリプトは同じテキストと 生成されたチェックサムを読み込んで htmlファイルに注入し 後でAppに表示します これらのフェーズの入出力の 依存関係の正確なセットが 宣言されていない場合 FUSE_BUILD_SCRIPT_PHASESがONのとき スクリプトを並行して実行します この問題のシナリオを詳しく見てみましょう 例えば"Generate HTML "には "checksum.txt "の入力宣言がありませんが 両スクリプトの他の入力と出力は すべて正しく宣言されているとします サンドボックスがなければ この設定ミスは気づかず ビルドに問題が発生する可能性があります これはFUSE_BUILD_SCRIPT_PHASESが オンになっていると Xcodeが両フェーズ間の 依存関係の推論に失敗し 並行して実行するように スケジュールされることを意味します ここにはいくつかの危険が潜んでいます checksum.txt が "Generate HTML" の入力依存 ファイルとしてリストされていないため クリーンビルドの際 スクリプトはファイルシステム上に存在しない このファイルを読み込もうとするのです もう一つの危険は "Calculate Checksum "の以前の実行により checksum.txtがディスク上に存在する場合 2つのスクリプトが並行して実行されると "Generate HTML "が古いファイルを 拾ってしまう可能性があることです これはユーザーエラーであり サンドボックスでスクリプトを実行すれば この問題を防げます サンドボックスをオンにした場合 「HTMLの生成」は checksum.txtを読み込もうとすると すぐに失敗します エラーメッセージはそのビルドフェーズに 不足している入力を追加することを案内します 入力と出力を正しく定義することで Xcodeは両フェーズ間の依存関係を尊重し 「チェックサムを計算」が「HTMLを生成」の前に 実行されるようにします 無関係なビルドフェーズが並列に実行できます シェルスクリプトを ターゲットで有効にするには ビルド設定エディタまたはxcconfigファイルで ENABLE_USER_SCRIPT_SANDBOXINGを YESに設定します 要約するとサンドボックス化されたシェルスクリプトは 正しい依存性情報を持つことで ビルドシステムが信頼できるため より速く 堅牢なインクリメンタル・ビルドを 可能にできるのです スクリプトのターゲットに対する ビルド設定を有効にすると プロジェクトのソースルート内ファイルへの アクセスがブロックされます プロジェクトでスクリプトの入出力として 明示的に定義されていない場合 サンドボックスは他のディレクトリへの 不正アクセスを防ぐことはできませんので セキュリティ機能とは考えないでください この機能を使用すると 既存のスクリプトフェーズの 入力または出力の欠落をデバッグし 有効な設定を確保することができます また先に説明したビルド設定 FUSE_BUILD_SCRIPT_PHASESと組み合わせて サンドボックス化によって依存関係が 正しく定義されたスクリプトフェーズは ビルドのクリティカルパスを短縮するため 並行して実行することができます ターゲット構築のステップを 並列化する方法は以上です 今度はArtemが 多くのターゲットを構築するための 並列化の謎を解き明かします ありがとう Ben さてここまででビルドシステムのタスクと プロジェクトでターゲットをビルドする際に 発生しうるフェーズの基本を説明しました よりグローバルな視点から Xcode がビルドで 最大限の並列性を引き出すために Swift ターゲット間の依存関係を どのように使用するか そしてプロジェクトの構造と構成がビルド時間に どのように影響するかを探ってみましょう プロジェクトにはいくつかの階層が 存在する可能性があります 例えばローカルライブラリの コレクションに依存するAppターゲットは セマンティック境界に沿って ターゲットに分割され いくつかのフレームワークで使用されます 各ターゲットは多くの異なるビルドフェーズ とステップを含み 他のターゲットのビルドフェーズとの間で ファイル依存性を生成および消費します プロジェクトの規模が大きくなると このタスクグラフのサイズや複雑さも 大きくなる傾向があります Xcode Build Systemは これらの階層をフラット化し ビルドをすべてのターゲットの ビルドフェーズに対応するタスクに分解します Swift ターゲットのための 特別なタスクはコンパイルです Swift ターゲットのソースコードを バイナリにビルドすることは 通常ビルド計画・コンパイル・リンクなど 多くのサブタスクで構成される複雑な作業です これらのタスクの調整は Xcodeツールチェーンである Swift Driverという 特別なツールに委ねられます ドライバーはターゲットの ソースコードに必要な コンパイラとリンカに対して 専門的な知識を持っています Swiftのコードを含むターゲットは コード配布の単位である モジュールにも相当します このターゲットのパブリックインターフェイスを キャプチャしたバイナリモジュールファイルは 下流のターゲットがコンパイルを開始するために 必要なビルドプロダクトです Swift Driverがターゲットの一つを 構築するために行う例を詳しく見てみましょう ターゲットは いくつかの ソースファイルの集合体です リリースまたは最適化されたビルドでは 機会を最大化するために コンパイラタスクをスケジュールします コンパイルタスクはSwiftモジュールを 生成します デバッグモードもしくは インクリメンタルコンパイルモードで Swift Driverは必要なコンパイル作業を より小さなサブタスクに分解し 並行して実行できるようにします Swiftのモジュールを作成するには 各コンパイルタスクの 部分的なステップが追加されます この例のようにターゲットに含まれる ソースファイルの数が多い場合 ビルドシステムのヒューリスティックに従って 個々のファイルをバッチコンパイルの サブタスクに割り当てられます ビルドログはソースファイルが 割り当てられたジョブにハイライトし 各ファイルの診断のための 個別のエントリを表示します 異なるソースファイル間で インクリメンタルビルドの 高速化と小規模化のために重要です Incremental Compilation Mode設定を 確認してください Xcode14以前ではXcode Build Systemと Swift Driverの境界のためターゲットの ビルドフェーズのオーケストレーションと 各ターゲットのDriverのインスタンスにより 生成されたコンパイルのサブタスクは 互いに独立して起こり それぞれのコンポーネントは 利用できるシステムリソースを最大限に 活用するため最善を尽くしました このビルドグラフの例で コンパイルフェーズの スケジューリングについて 詳しく見ていきましょう Swiftのターゲットの依存関係は 依存先の公開インターフェースを捕捉する バイナリモジュールを 提供することで解決します この依存関係を解決すると 次のような順序になります 各ターゲットのトップレベル Swift Driverタスクと 個々のサブタスクが 時系列で表示されるようになりました Xcode14では Swift Driverの 全く新しい実装のおかげで ビルドシステムと コンパイラが完全に統合されています Xcodeビルドシステムは コードをコンパイルするため 実行しなければならない スケジューラの機能をします この計画メカニズムによりXcodeは きめ細かい スケジューリング決定を行うことができ プロジェクトの構築においてCPUを過剰に使用し システム全体のパフォーマンスを 低下させることなく 利用可能なリソースのみを使用して 確実に保証します
これまでXcodeビルドシステム管轄外の サブタスクの集まりだったものが 完全にビルドシステムの スケジューラの領域となったのです
タスクプールに個々のサブタスクが すべて存在するため ビルドスケジューラによる トレードオフの考慮が重要です 例えば 8コアのマシンでは 依存関係が満たされた
利用可能なタスクを 8つの実行スロットのうちの1つに割り当てます ビルドシステムはスロットの1つが空くと そのスロットをより多くの 優れた仕事で埋めようとします コア数の多いマシンで より多くの同時作業が可能になります しかし それは より多くの作業を実行できる アイドルコアが存在する可能性も 高くなるということです しかし すべての未処理タスクは 現在実行中または待機中の 他のタスクによって生成される入力を まだ待っている状態なのです 新しい統合ビルドシステムはスケジューラーが このアイドル時間を大幅に 短縮することを可能にします その方法を見るために ターゲットのコンパイルの 依存関係バイナリモジュールが どう解決されるか再検討しましょう
先ほどから取り上げているように コンパイルのサブタスクの部分的な結果を ターゲットの最終的なモジュールに マージします Xcode14とSwift5.7の新機能として ターゲットのモジュールの構築は プログラムソースから 直接別のemit-moduleタスクで行われます 依存関係のあるターゲットの 他のコンパイラタスクを待たずに emit-moduleタスクが完了すると同時に ターゲットの依存関係の コンパイルを開始できます 下流ターゲットコンパイルの ブロックを解除することで アイドル状態のCPUコアの作業待ち時間を 短縮することができるのです
これをプロジェクトの他の部分に当てはめると 全体の作業量は同じでもビルドシステムは コンピュータのリソースを より効率的に使用でき ビルドを大幅に高速に 完了できることがわかります
さてSwiftのビルド時に ビルドシステムが実行できる 2つ目のクロスターゲットの 最適化を見ていきましょう Eager Linkingです 前の例に基づいて 各ターゲットのリンカータスクを追加しました どちらもビルドの クリティカルパス上にあります この場合 対象Bは対象Aをリンクするため 対象Bのリンクタスクは 対象Aのリンク出力が生成され コンパイルタスクが完了してから実行します イーガーリンクでターゲットBのリンクタスクは Aのエミットモジュールタスクに依存できます その結果 Bは早い段階でリンクを開始し Aのリンクと並行して実行でき クリティカルパスを短縮できます この仕組みはどうなっているのでしょうか? 通常 製品の依存関係がリンクしている 2つのターゲットの依存関係グラフは 次のようになります 依存するターゲットをリンクするには ターゲットのコンパイル出力に加え 依存するターゲットのリンク済製品が必要です イーガーリーリンク時この依存関係は解消され 依存するターゲットがより早く開始できます 依存関係のリンクされた 製品に依存するのではなく emit-moduleタスクによって ビルドプロセスの早い段階で生成される テキストベースのダイナミック ライブラリ スタブに 依存するようになります リンク先の製品で依存関係で使用するために このスタブにはのシンボルの リストが含まれています この最適化はXcodeのこのビルド設定を使って 有効にすることができます Eager Linkingは依存関係によって 動的にリンクされるすべての純粋な Swiftターゲットに適用されます 要約すると Xcode Build Systemは ビルドフェーズを並列に実行することで 可能な限りの並列性を引き出そうとする 高度なスケジューリングエンジンです また スクリプトサンドボックスなどの 機能により ビルドの並列性と信頼性を 最大限に高められます XcodeとSwiftは これまで以上に統合されています そしてプロジェクトの構造:そのモジュール化 ターゲット製品間の依存関係からなる グラフの全体的な形 およびそれらの中の ビルドフェーズの数と複雑さ あなたのマシンの利用可能な 計算リソースとの組み合わせ これらはすべてXcodeがあなたのビルドを 並列化して 高速化に寄与する要因です この知識と ビルドタイムラインのような強力な 新ツールで プロジェクトを検証する能力を備えています さらに技術的な裏側を知りたいと思われる方は 今回説明したXcodeで使われている技術の多くは オープンソースで 開発されています リポジトリは以下リンクから GitHubにアクセスできます Xcodeに関する他のセッションについては “Xcodeの最新情報”で 今年の新機能と改良点をチェックしてください Xcode14のリンク時間を最大2倍改善する方法は 素早くリンクする:ビルドや起動時間の改善 フォローありがとうございました Xcodeビルドの新しい発見が あったのではないでしょうか どんな作品を作るか楽しみですね
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。