ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Appサイズとランタイムパフォーマンスの改善
SwiftやObjective-Cのランタイムを最適化して、Appを縮小化したり、高速化したり、短時間で起動したりできるようにする方法を紹介します。Xcode 14でのAppのビルドや、デプロイメントターゲットの更新によって、効率的なプロトコルチェックや、メッセージ送信コールの縮小、ARCの最適化を実現する方法をご確認ください。
リソース
関連ビデオ
WWDC22
-
ダウンロード
♪ ♪
こんにちは Ahmedです ClangとSwiftの コンパイラを担当しています このセッションでは Swiftと Objective-Cの一般的な操作をより速く より効率的にするための変更を 深く掘り下げ Appサイズと ランタイムパフォーマンスを 改善できるようにします SwiftやObjective-Cで コードを書くとき 常に2つの主要な要素と やりとりしています まず Xcodeを使って ビルドするのですが その際にSwiftとClangの コンパイラを使用します しかし Appを実行するとき 重い作業の多くは SwiftとObjective-C ランタイムで行われます ランタイムは 全プラットフォームの OSに組み込まれています コンパイラがビルド時にできないことは ランタイムがランタイムに行うのです 今回は コンパイラと ランタイムの両方で行った いくつかの改良点 について紹介します 今回のセッションは少し特殊で 新しいAPIや言語の変更 新しいビルド設定 などはありません コード変更の必要がないため これらの改善はすべて デベロッパのみなさんにとって 透明性の高いものです 早速始めましょう 今回は 4つの改善点を ご紹介します Swiftのプロトコル チェックを効率化し Objective-Cのメッセージ送信 の呼び出しもretainと releaseの呼び出しと 同様に小さくし 最後に自動リリースエリジョンを 高速化 小型化します 詳しく見ていきましょう
まずは Swiftでプロトコル チェックをしてみましょう
ここでは CustomLoggable プロトコルを使用しています 読み取り専用の計算された プロパティcustomLogStringを持ち CustomLoggableオブジェクトの 特別な処理を持ち 私たちのログ関数で それを使用できます 後でEvent型を 名前とデータ フィールドで定義します そして customLogStringプロパティの ゲッターを定義することで CustomLoggable プロトコルに適合します
log関数にオブジェクトを 渡せるようになります このコードを実行すると log関数は渡された値が プロトコルに適合しているか チェックする必要があります as演算子でそれを実現しています is演算子も知っているかもしれません
可能な限り この確認はコンパイラ でビルド時に最適化されます しかし まだ十分な情報が 得られていないこともあります その為 先に計算したプロトコルの メタデータを利用し ランタイムで行う必要が あることが多いです このメタデータで ランタイムは 特定のオブジェクトが本当に プロトコルに適合しているかを 知ることができ チェックは成功します
メタデータの一部は コンパイル時に構築されますが 多くのメタデータは起動時 にしか構築できません 特にジェネリクスを 使用する場合はそうです 多くのプロトコルを使用する場合 これは数百ミリ秒になることも App起動時間の半分程度が この処理に費やされています 新しいSwiftランタイムでは App実行ファイルと起動時に使う dylibのための dyldクロージャの一部として 前もってこれらを 計算します 何より iOS 16 tvOS 16 watchOS 9で 動作している場合 既存Appでも有効になります dyldとlaunch closures についてもっと知りたい方は 「App起動時間:過去 現在 未来」をご覧ください Swiftでのプロトコル チェックでした
メッセージ送信に 移りましょう Xcode 14の新しい コンパイラとリンカによって ARM64ではメッセージ送信の 呼び出しが12バイトから 最大8バイトまで 小さくなりました メッセージ送信は あらゆる場にあり これが積み重なって バイナリでは最大2%の コードサイズ改善が見られました Xcode 14でビルドする場合 デプロイメントターゲットとして 古いOSリリースを使用しても 自動的に有効になります デフォルトでは サイズと性能 のバランスが取れていますが objc_stubs_small リンカフラグを使用し サイズのみを 最適化することが可能です 何が変わったのかを 見てみましょう では まず例を 挙げてみましょう ここでは 会議開始日の NSDateを作ろうとしています まず NSCalendarを作り NSDateComponents を記入します それを日付にし 最後に返します コンパイラが生成する アセンブリを見てみましょう さて アセンブリの詳細は 重要ではありません コンパイラの人間が一日中見つめ みなさんがそうする必要はないのです 重要なのは 日付コンポーネントのように プロパティアクセスを 行う場合も ほぼすべての行で objc_msgSendを呼ぶ 命令が必要になることです コンパイル時にはどの メソッドを呼ぶべきかわからず objcランタイムだけが 知っているためです そこで objc_msgSendを使い ランタイムを呼び出し 正しいメソッドを 見つけるよう依頼します その中の1つのコールに 着目してみましょう objc_msgSendを呼び出す命令は 既に述べたとおりです しかし それだけでは ありません どのメソッドを呼ぶかを ランタイムに伝えるため objc_msgSend呼び出しに セレクタを渡す必要があります これには セレクタを準備する為 もう2つの命令が必要です バイナリを見ると これら命令は それぞれスペースを取っています ARM64では それぞれ4バイトになります つまり これらの objc_msgSendを呼ぶ毎に 12バイトを 使用しているのです: とても積み重なります それを改善するために 何ができるか考えてみましょう
さて 先ほど見たように このうち8バイトは セレクタ準備に充てられます 面白いのは どのセレクタでも 常に同じコードであることです ここで最適化が適用できます これは常に同じコードなので 共有することができ メッセージ送信のたびに 発行するのではなく セレクタごとに一度だけ 発行することができます それを取り出して 小さなヘルパー関数に入れ 代わりにその関数を 呼び出せばいいのです 同じセレクタを 何度も呼び出すことで これらの命令バイトを すべて保存することができます このヘルパー関数を "セレクタスタブ "と呼びます
まだ本物のobjc_msgSend関数 を呼び出す必要があるので そちらに進みます 関数自体のアドレスを ロードして呼び出すために さらに別のインダイレクトを 持っています 詳細は重要ではありませんが 重要なのは そのために もう数バイトのコードが 必要だということです
前述した通り ここでどのモードにするかを 選択することができます この2つの小さなスタブ関数は 別々にすることもできます 私たちは 最も多くの コードを共有し これらの機能を できるだけ小さくできます 残念ながら これでは2回の呼び出しを 背中合わせに行うことになり パフォーマンス的に 理想的ではありません そこで 別バージョンでさらに 改良を加えることができます 2つのスタブ関数を組み合わせ 1つの関数にできます そうすれば コードを近づけ 呼び出し数も少なく済みます この右側にあるのがそれです
この2つの 選択肢があるわけです サイズだけを最適化し 最大限のサイズ削減効果を 得るかどうかを 選択することができます -objc_stubs_smallリンカフラグで 有効にすることができます または 最高の パフォーマンスを維持しながら サイズメリット提供コード生成を 使用することができます よほどサイズに制約がない限り これの使用をおすすめしますし それがデフォルトに なっている理由です スタブを使ったより小さな メッセージ送信でした もう一つの改良点は retain/releaseを安くしたことです Xcode 14の 新しいコンパイラでは retain/releaseの呼び出しが ARM64の8バイトから 最大4バイトまで 小さくなりました すぐに分かるように メッセージ送信と同じように retain/releaseも いたるところにあります これが積み重なり バイナリコードサイズが 最大で2%改善されたわけです メッセージ送信スタブとは異なり ランタイムサポートが必要で iOS 16 tvOS 16 または watchOS 9のデプロイメントターゲットに 移行すると 自動的に取得されます 何が変わったのかを 見てみましょう 例に戻りましょう msgSendコールの 話をしましたが ARC(自動参照カウント) の場合 コンパイラが挿入する retain/releaseコールも 多くなってしまいます ハイレベルでは オブジェクトへの ポインタのコピーを作成するたび それをライブに保つため 保持カウントを インクリメントする必要があります 変数cal dateComponent theDateでこの処理を行います objc_retainを使ってランタイムを 呼び出すことで行っています 変数がスコープ外に出たら objc_releaseを使い retain count をデクリメントする必要があります ARCの利点の1つは これら 呼び出しを最小限に抑えるため 多くの呼び出しを排除する コンパイラマジックです このマジックのひとつに 後ほど少し入っていきます しかしどんなマジックを使っても このようなコールは必要です この例では calendarと dateComponentsのローカルコピーを 解放する必要があります
裏を返せば これら objc_retain/release関数は 単なるC関数です: 単一の引数 解放される オブジェクトを取ります ARCでは コンパイラがこれら C関数の呼び出しを挿入し 適切なオブジェクト ポインタを渡します これら呼び出しは プラットフォームの Appバイナリインターフェース で定義されたCの呼び出し 規約を尊重する必要があります 具体的にこれら呼び出しを行うには 正しいレジスタにポインタを渡す為 さらに多くのコードが 必要になるということです そのためだけにいくつかの 「移動」の指示を追加します そこで 新たな 最適化を行います retain/releaseを独自の呼び出し 規則で特殊化することで オブジェクトポインタが すでにある場所に応じ 適切なバリアントを 臨機応変に 使用することができます これらの呼び出しのための 冗長なコードの束を 取り除くということです また このちっぽけな 指示には大したことが ないように 見えるかもしれませんが App全体となると 大きな負担となるのです そうして retain/releaseオペレーションを 安くしていきました 最後に オートリリース エリジョンについてです こちらはさらに興味深いです objcランタイムの変更に伴い 自動リリースのエリシオンが高速化され 既存のAppを新しいOSで 実行すると 自動的にそうなります その上でさらに コンパイラを変更することで より小さなコードにしました iOS 16 tvOS 16 またはwatchOS 9の デプロイメントターゲットに移ると サイズメリットを自動的に得られます
さて そもそもオートリリース エリジョンとは何でしょう? 例に戻りましょう 先程 ARCはリテンションと リリースを最適化するため 多くのコンパイラマジックを 提供すると述べました 1つのケースに絞って 考えてみましょう: オートリリース された戻り値です この例では 一時的なオブジェクトを作成し 呼び出し元に返しています では その仕組みを 見てみましょう 一時的なtheDateがあり それを返し 呼び出しが完了し 呼び出し元は それを自身の変数に保存します では ARCでどうなるか 見てみましょう ARCは呼び出し側にretainを挿入し 呼び出された関数にreleaseを挿入します 一時的なオブジェクトを返すとき スコープの外に出るので 関数の中で最初にそれを 解放する必要があります しかし まだ他のリファレンスを 持っておらず それはできません もし出したとしても 戻る前に壊されてしまうので それはダメです テンポラリを返せるようにするため 特別な規約が使われています 呼び出し元が保持できるよう リターン前にそれを自動解放します autoreleaseとautoreleasepoolは 見たことがあると思います: これは単にリリースを 後に延期する方法です ランタイムはリリース時間を 保証するものではありませんが 今ここでリリースされない限りは この一時的なオブジェクトを 返すことができるので 便利です さて これは 無料ではありません オートリリースを行うには ある程度オーバーヘッドが必要です そこで登場するのが オートリリースエリジョンです その仕組みを理解するために アセンブリを見て この戻りを辿ってみましょう autoreleaseを呼び出すと それがobjcランタイムに入り そこから楽しいことが 始まります ランタイムは起こっている事を 認識しようとします: autoreleasedされた 値を返しています それを助けるために コンパイラは 他では決して使わない 特別なマーカを出力します ランタイムに 自動リリースエリジオンの 対象であることを伝えるためです そして その後に続くのが 後で実行するretainです しかし 今はまだ オートリリース中で 実行すると ランタイムはデータとして 特別マーカ命令をロードし それが期待する特別マーカ値で あるかどうかを比較します もしそうなら コンパイラはランタイムに すぐに保持される一時的なものを 返すと伝えたことになります 一致するautoreleaseとretainの 呼び出しを省略 削除ができます それが自動リリース エリジョンです
しかし これも無料ではありません: コードをデータとして読むことは それ以外では一般的なことではなく CPUに最適とは 言えません もっといいものが できるはずです そこで 今度は新しい方法で もう一度リターンシーケンスを たどってみましょう まずは 自動リリースからです やはりObjective-Cの ランタイムに入ります ここで すでに貴重な情報があります: 返送先アドレスです この関数の実行が完了した後 どこに戻る必要があるかを示します それを記録して おくことができます ありがたいことに 返送先アドレス の取得は非常に安価です 単なるポインタで サイドに格納できます その後 ランタイムの 自動解放コールを残します 呼び出し元に戻り retainを行う際に ランタイムに再入力します ここからが新たな ビックマジックの始まりです その時点で 現在地を見て 現在の戻り先へのポインタを 得ることができます ランタイムでは retain時に取得したポインタと 以前autorelease時に 保存したポインタを 比較することができます しかも 2つのポインタを 比較するだけなので これはとても安価です 高価なメモリアクセスは 必要ありません 比較に成功すれば autorelease/retain のペアを省くことができ パフォーマンスを向上させることが できることがわかります
その上 もうこの特別な マーカー命令を データとして比較する 必要がなくなったので 削除すればいいのです また コードサイズも 節約できます そうして 自動リリースエリジョンの 高速化と小型化を実現しました SwiftとObjective-Cの ランタイム改良は何度も行いました まとめましょう 新しいOSで Appを実行する場合 ランタイムの改善により Swiftのプロトコルチェックは より効率的に行われます 自動リリースエリジョンをしようとすると 毎回それも速くなります Xcode 14の新しいコンパイラとリンカ メッセージ送信スタブのおかげで Appを再ビルドすることで コードサイズを最大2%削減できます 最後に デプロイメントターゲットを iOS 16 tvOS 16 watchOS 9に アップデートする際 retain/releaseの呼び出しを 小さくすることで さらに2%節約ができます 自動リリースのエリジョンシーケンスが 小さくなったおかげで 尚更です SwiftとObjective-Cランタイムの 深堀りは楽しめましたか? ご視聴ありがとう ございました
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。