ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
ClangとLLVMの新機能
このセッションでは、ClangコンパイラとLLVMの向上によって可能となった、C、Objective-C、C++の最新のエンハンスメントについて紹介します。また、新しい静的解析機能と、それらを使用してコードを改善する方法、そしてコードサイズのための新しい最適化機能を活用する方法も説明します。
リソース
関連ビデオ
WWDC21
WWDC19
-
ダウンロード
(音楽)
(拍手) 私たちはこの1年間 コンパイラへの 新機能追加に取り組みました ジェシカです 今日は同僚のJFとデヴィンと3人で 新機能をご紹介します 項目は多岐にわたります まずは新しいプラットフォームサポート 次に 低レベルのコードサイズの最適化 言語レベルのコードサイズの最適化 新しい診断機能 最後はコードのバグ検出に役立つ スタティックアナライザのチェックです では新しいプラットフォーム サポートについて― Series 4 Watchを例にお話しします
Series 4 Watchは 通常64ビットチップです
しかしApp Storeのアプリケーションは すべて32ビットです
WWDC1日目のセッションで― アプリケーションがSeries 4 Watchで シームレスに動くのを見せました
再コンパイルしていないのに すべて動いたのは不思議ですね
なぜでしょう? 皆さん よろしければ 足踏みでドラムロールをお願いします
答えはBitcodeです (拍手) 初めての登場です ご説明します
お好きな言語で ソースコードを コンパイラに渡すとします 通常は標準的な コンパイルプロセスを行い― バイナリなどが生成されます この場合は違います コンパイルプロセスを 早い段階で止めて― LLVM Bitcodeを生成します
LLVM Bitcodeが便利なのは コンパイラの中間コードとなる点です
この中間コードを使うと― コンパイルプロセスを 途中からでも始めることができます 今回はそれを App Storeで行っています
1つのBitcodeから 2種類のアプリケーションができます 1つは32ビットチップで動作し― もう1つは新しい 64ビットチップで動作します
1つ問題があります コンパイラではコードが 64ビットデバイス用とは認識されません 認識されればその情報を アプリケーションの最適化に使えます 回避策として64ビットチップ用の Bitcodeも用意し― アプリケーションを 素早く生成できるようにしています まるで魔法のように― コンパイラが動いているように 見えますね
コードサイズの改善に話を移しましょう コンパイラの生成コードを 小さくするための改善です コードサイズは重要です 大きいとダウンロードに 時間がかかります 大きいアプリケーションは デバイスの容量を使います 何よりもコードサイズを重視するという ユーザの方々のために― Xcodeに最適化レベルを追加しました その最適化レベルとは― -Ozです ゼッドと発音しますが カナダ人だからと思ってください (拍手) 今日は-Ozを使った 最適化の例をお見せします ただし その前に― コンパイラの仕組みを 数分で簡単に説明します
コードをコンパイルする時― 最初の表現つまりソースコードは ターゲットに依存しません これをコンパイラにかけると IRつまり中間表現に変換されます 中間表現も大抵は ターゲットに依存しませんが― 依存する部分も含んでいます 一見 汎用的なアセンブリです ここでプロセスを止めて Bitcodeを出力することもできますが― このまま続けましょう
先ほどの中間表現をさらに MIRつまりマシンIRに変換します 最後に生成されるマシンIRは ターゲットのアセンブリとほぼ同じです ここではArm 64アセンブリです これをside-by-sideにもできます
これからお話しする最適化では マシンIRを使いますが― 分かりやすいように 例はすべてアセンブリにしています
関数のアウトライン処理について お話ししましょう これはコードサイズを小さくする 最適化手法の1つです コンパイラで最後のほうに行われ ソースコード言語には依存しません 例を使って説明します このようなアセンブリがあるとします hasseとkakutaniは 任意のプログラムの2つの関数です 面白いことに この2つは 同じ命令を指定しています この同じ命令を抜き出して 新しい関数に入れることができます そして この新しい関数を― 呼び出しやブランチで見つかった シーケンスの代わりに使います これでプログラムの サイズを削減できます 削減できる割合は― テストプログラムでは最大25%でした
(拍手) どうして これだけ 削減できるのでしょうか? コードにコピー部分が多く リファクタリングが必要だから? そうではなく より深いところの話です コピー部分が多ければ アウトライン処理に影響しますが― それほど重要ではありません 別の例を見ましょう この関数があるとします どのような処理をするかは さておき― この関数をコンパイラにかけて アセンブリができるとします 出力はこのようになります このアセンブリを 理解する必要はありません ただし関数の最初と最後の 命令を見てください
関数プロローグ エピローグと言いますが― これらはソースコードの どの行にも対応しません コンパイラがシステム要件に沿って 挿入した命令です ここにあるようなストアやロードの 命令は プログラムの各所に出現します アウトライン処理ではこれを利用し プログラム全体のサイズを縮小します
注意事項があります
そもそもアウトライン処理では プログラムの制御フローを変更します 例えばulamからcollatzへの 呼び出しをアウトライン処理すると― プログラムの制御フローを 変更することになります 問題は collazの内部で クラッシュした場合にどうなるかです プログラムをLLDBにかけてみましょう 追加されたアウトライン関数が バックトレースに含まれます アウトライン処理の際は これに注意が必要です
プログラムの実行時間が 長くなる可能性もあります
呼び出しの追加は 実行時間に影響します -Ozを使うのは 何よりサイズを 小さくすることが目的なので― 実行時間は二の次になります
パフォーマンス重視のコードの場合 -Ozの使用はお勧めしません 実行時間重視のプログラムには 最善ではありません ただしInstrumentsはお勧めします プログラムのホットスポットを 特定できるので― アプリケーションをどう最適化するか ヒントを得られます コンパイラでは 多くのレベルで最適化が可能です 何を優先するかは レベルによって違います
例えば-Ozは とにかくサイズを優先します その結果 実行時間が 少し長くなる可能性があります 対照的な位置にあるのは― -O3です プログラムの実行時間を優先するので サイズが大きくなる可能性があります
-OsはXcodeの デフォルトの最適化レベルです 速度とサイズの バランスをとって最適化します 他の要件がある時は Instrumentsを活用できます
コンパイラには 追加の最適化機能があります 時間はありませんが簡単にご紹介します まずはPGOつまり プロファイルに基づく最適化です PGOを使う場合は 実際にプログラムを実行して― 実行に関する情報を収集します その情報を基に もう一度 コンパイルできます 次はLTOつまりリンク時最適化です コンパイル時間は少しかかりますが コンパイラにこう命令できます “プログラムのファイルが すべて そろってから―” “インラインとアウトラインの処理を 最適に行いましょう”と インラインとアウトラインのような 最適化は― コンテキストが多いほど うまくいきます 使用する最適化レベルに 追加の最適化機能を組み合わせれば― いいパフォーマンスを引き出せます
具体的に説明する時間はないので― LLVMの最新情報に関する 前のセッションをご覧ください
Xcodeで-Ozを有効にするには― プロジェクトのビルド設定で 最適化レベルに“-Oz”を選択します 特定のファイルで 機能を有効にすることもできます プロジェクトのビルドフェーズから コンパイルソース一覧を開き― コンパイラフラグを設定します
あとはアプリケーションの コードサイズをどう調べるかですが― コードサイズを知るには sizeというツールをお勧めします ターミナルのアプリケーションです アプリケーションのバイナリに関する 低レベルの情報を入手できます アセットなどは含まないので 実際の合計サイズは分かりません 概要とhello worldのような コードだけの場合― コンパイラは助けになりません
sizeを使ってみましょう “size”とバイナリのパスを 指定するだけです 出力はこのようになります バイナリの各セグメントのサイズと バイナリの全体のサイズが分かります ただしバイナリの各セグメントにも 多くのセクションがあると思います この例では 実行命令の部分のみを考えるとして― sizeに追加のフラグを指定します -lというフラグと-mというフラグです これでセクションの 内訳サイズが分かります 実行命令について知りたければ textセクションを確認します
以上が アプリケーションの コードサイズを調べるためのヒントです 次は私のカナダ人の同僚 JFが― 言語レベルの コードサイズ改善についてお話しします (拍手) ありがとう ジェシカ (拍手) JFです 言語レベルの 最適化についてお話しします 先ほどは低レベルの アセンブリなどが対象でした 次は言語自体 つまりコードの中身の話です コードサイズにも影響する 4つの最適化をご紹介します 最初はObjective-Cで ブロックを使う場合です ブロックにはコンパイラが生成する メタデータや― ヘルパー関数が関連付けられます いくつか例をお見せしましょう 次のようなコードを書いたとします 2種類の関数の 2つのブロックがあります コードはともかく ブロックの処理が異なることに注目です コードは互いに無関係ですが ブロックのキャプチャ構造が似ています 2つのARC strongポインタを キャプチャしています 各ブロックにはメタデータが 関連付けられています これがそのメタデータです ブロック使用時に コンパイラで自動生成されます ブロックの情報と 言語で保証される動作を記述しています block sizeがありますね 2つのメソッド copy helperと destroy helperに― block method signatureと block layout infoもあります これはコンパイラで生成される 合成コードです 難しそうに見えますが構造は同じなので 多くの場合は重複を削除できます ただ この例ではできません 2つのARC strongポインタ以外の キャプチャがあるのと― ブロックサイズ自体が違います このケースではマージできませんが 基本的にこうしたものはマージ可能です 気づいた方もいるでしょうが―
copy helperとdestroy helper関数は マージ可能です Xcode 11以降は これらをマージしています copy helperは ブロックの移動に使う関数で― destroy helperは ブロックの破棄に使う関数です コンパイラでコードが合成されると 次のようになります 2つのARC strongポインタがあります コードとして コピーにはretain 破棄にはreleaseが生成されます この時 ブロックで必要な処理は 他にも考えられます C++オブジェクトがあるなら コピーコンストラクタや― デストラクタが必要でしょう ARC weakポインタや Cの非自明型などもあるなら― それぞれの処理が必要です 基本的にはブロックに冗長性があれば コンパイラで排除されます この結果として― Objective-Cアプリケーションでは コードサイズが2~7%削減されます 無料のデフォルト機能です (拍手) 2つ目の最適化は― NSObjectの直接のサブクラスの インスタンス変数に関係します 言いにくいですね 例を挙げて説明しましょう カードゲームのコードがあります これはNSObjectから 直接 派生させています Objective-Cコードでプロパティは インスタンス変数と対応します インスタンス変数が バックアップとして自動生成されます クラス自体についてはコンパイラは このような構造体を生成します これがメンバのレイアウト順です Objective-Cでは 派生元の基底クラスから― コードを 別のフレームワークに変更できます 基底クラスのメンバが新しくなっても 派生クラスは壊れません C++の場合は派生させると― 基底クラスのレイアウトが変わり サイズは新しくなります これはNSObjectからの派生です NSObjectはプラットフォームの ABIの一部なので変化しません このレイアウトのクラスを initWithNameメソッドで実装します クラスの全メンバの レイアウトは分かっています そのためXcode 11以降のコンパイラでは オフセットをハードコードできます このinitWithNameメソッドについて 説明しましょう
self.name = nameがあります コードを生成するセッターは Xcode 11以前はこうなります テーブルへのルックアップを合成し nameのIVARのオフセットを取得します Xcode 11からは別の方法をとっています NSObjectの直接の派生メソッドを 実装する時は― オフセットを単にハードコードします 書かれたものを そのまま実装するのです 3つのうち1つの命令であるものの― アプリケーションのサイズを 約2%削減できます うれしいですよね (拍手) 次は C++の型のデバッグが 容易になったことについてです 実はこれもサイズ最適化の話なのです 例えばコードを書きます 簡単なコマンドライン アプリケーションです コマンドラインから パラメータを文字列として取得し― std::vectorに入れ 整数に変換した後 順に出力します 簡単なデモアプリケーションです 標準ライブラリの型を 使っている点に注目です 配列のpush backを使い ここにブレークポイントを置きます Xcode 11以前 この処理には問題がありました push backのようなlibc++メソッドは 以前は強制的にインライン指定して― 表示を制御していました 問題は 大きなプッシュバックの中身が 最適化で間引きされることです デバッグ用に 単にpush backの行に ブレークポイントを置くと― 中身があちこちにあるので 正確なブレーク位置が分かりません Xcode 11以降は 強制インライン指定はせず― インライン処理の必要性を アルゴリズムで判定します Xcode 11以前では この例のブレークポイントは― プッシュバックの中身が散っているので 実際 2回目のループで呼び出されます 強制インライン指定をしないXcode 11の デバッグセッションをお見せしましょう LLDBとプログラムを実行し ブレークポイントを12行目に指定します デバッガでブレークポイントが 認識されます 実行すると このようになります 12行目でブレークしたので 問題ありません 以前は想定どおりにはいきませんでした 大きな部分をインラインにしなければ コードサイズも最適化できます STLを多く使うと コードが膨らんでしまいます 大きなアプリケーションでは リリースモードで― コードサイズを 最大7%削減できます リリースモードにおいてです かなりの削減になり デバッグもうまくいきます 最後の方法は C++の静的デストラクタの抑制です ここでも例を使って説明します 汎用的なC++コードです 大抵のアプリケーションでは ロガーを使用しますが― あちこちでロガーを渡すより グローバル変数Loggerを使います C++でこうした グローバル変数がある時は― アプリケーションの存続期間の最後に デストラクタが実行されます std::vectorの文字列で バッファが含まれていますね デストラクタはこれを破棄します ここまでは簡単です これはゲームなので ゲームのコードを追加します 1つのアプリケーションに 1つのゲームです ここでゲームの グローバル変数を使っています 問題はログ用のコードを ゲームの構造体に追加する時です LoggerもGameもグローバルであることが 問題につながります 異なる翻訳単位間での デストラクタの呼び出し順は― C++では保証されません Gameの前にLoggerが破棄されれば クラッシュが起こりかねません ここが頭痛の種で C++の仕組みを考えていくと― このように 深みにはまってしまいます スレッドローカルストレージや スレッドを追加したりするでしょう C++デストラクタの順序付けは複雑で 仕組みを知っている人たちも混乱します 数ヵ月前 Clangでバグを修正しました 終了時のクリーンアップでの クラッシュという珍しいケースです つまり破棄の順序を修正するのは 簡単ではないという話です iOSでのアプリケーションの ライフサイクルをご覧ください シャットダウンに 論理的タイミングはありません フォアグラウンド バックグラウンド 非動作の状態があります 破棄と同じく シャットダウンは このライフサイクルに当てはまりません 結局はコールバックを実装し― バックグラウンドへの移行 復帰などの命令を受け取ります デストラクタは論理的な場所で 実行されません 前のアプリケーションのLoggerでも― バッファをフラッシュする 論理的タイミングはありません デストラクタと同様です バックグラウンドに移行するなら 先にバッファをフラッシュします するとデストラクタでの クリーンアップは不要になり― デストラクタは意味がなく 無用なコードになります そのためXcode 11では 破棄を抑制する属性を追加しました グローバル変数に デストラクタは不要です もちろんコールバックがあれば 手動でフラッシュします Xcodeでアプリケーション全体に この設定を使用できます C++の割合によっては コードサイズが1%削減されます コードサイズの削減から 診断に話を移しましょう Xcode 11には5つの診断機能が デフォルトで搭載されています まず コンストラクタまたは デストラクタからの― 純粋仮想関数の呼び出しについてです Tableから始まる オブジェクト指向コードがあります 純粋仮想関数を指定してみましょう このgalahadは純粋仮想関数です Tableのデストラクタも指定し 破棄の際 galahadの行き先を探します 問題なさそうですが警告が返されます Xcode 11で この警告が出る理由ですが― コンストラクタやデストラクタから 純粋仮想関数を呼び出すのは無意味です Tableは基底クラスで この場合 派生クラスは ほぼ破棄されています galahad関数の 呼び出し対象は存在しません 修正には galahadを実装する 派生クラスを使います そのデストラクタでgalahadを探せば オブジェクトが返されるでしょう では次の診断機能です
memsetの引数の順番違いについてです Inboxという構造体があるとします 休暇明けにInboxのEメールを消すため memsetを使ったコードを書きました さて どこが問題でしょう? memsetの引数の順番が違います 順番を覚えていないので たまに この間違いをします 破棄しようとしている引数の値が 何番目であろうとサイズが何であろうと Xcode 11では順番の違いが検出されます 修正には引数の順番を変えるだけです memsetの場合 コードを見ても 順番が正しいか分からないので― 状況によっては std::fillを使うこともできるでしょう このほうが間違いにくく 処理も簡単に分かります
3つ目はreturnでの std moveに関する警告です C++では少し複雑なmoveも 適切に使えるよう診断機能があります では 例となる オブジェクト指向コードです Lion Goat Snakeという 3つの構造体を― オブジェクト指向で Chimaeraに入れます bellerophonを割り当てて chimaeraをslayし― 確認のためchimaeraを返します すると診断が表示されます chimaeraで実際に返される型は Goatだけと言っています chimaeraから配列を切り取り Goatに入れるのは無意味なので― 配列をコピーします このコードはstd::moveに関して 教科書どおりのことをしています つまりコピーの省略です std::moveはreturnでは大抵 不要ですが ここはコピーなので必要です returnと言ってクラスの一部を 取り出すのは妙ですが― moveは暗黙的であってはなりません 警告は意図どおりか確認しています 修正にはstd::moveを呼び出します
そうすると配列がGoatに移動します こう移動するほうが効率的です Goatだけ返すと友人が不信感で キマイラになる恐れがあるなら― コピー省略で chimaeraだけを返すのもいいでしょう std::moveを追加すると おそらく コンパイラは非効率性を指摘します chimaeraを取得するか不明な時は― std::optionalで chimaeraを返す方法もあります この場合クラスからの切り出しは行わず 暗黙的にコピー省略となります 次はポインタサイズの 割り算に関する診断です コードの例を挙げましょう 問題はないように見えます 配列がありますね 0番目の要素で配列のサイズを割ると 配列の要素数が分かります Cスタイルでよく使われる お決まりのコードです 問題はこのコードを リファクタリングする場合です 配列を 代わりに パラメータとして渡しましょう Cのルールでは 配列はポインタに変換されます 新しい診断機能はここで― 配列の要素数が 返されないことを指摘します この問題が検出されるのです 具体的な修正方法としては― お決まりのコードの代わりに 例えばstd::sizeを使います 間違った方法で リファクタリングする前に― 問題を修正できます std::sizeは目的にかなっています これはエラーを捉える適切な警告です 最後はデフォルト宣言した 関数での削除についてです コードの例を挙げましょう Aberrationという構造体に― float型のeyestalksと eyeそしてmouthがあります Aberrationを デフォルト宣言すると― “デフォルトのAberrationが 分からない”と警告されます float参照があり コンストラクタを合成できないのです 参照型にはデフォルトを作成できません この場合 C++では参照以外にも 多くの指定方法があります ここでの警告は― 頼まれたデフォルトコンストラクタを 提供できないことを示しています 1つの修正方法として コンストラクタを自分で作れば― eyestalksを渡した時に 参照が自動作成されます 個人的に このAberrationのコードは― float参照を使わずに 指定するほうがいいと思います これでデフォルトを作成できます 診断機能については以上です デヴィンがスタティックアナライザの 新しいチェックについて説明します (拍手) 先ほどは コンパイラでビルド時に 表示される警告の話でした バグを検出するツールは他にもあります その1つがスタティックアナライザです
アプリケーションを実行しなくても コードの深部にあるバグを検出できます テストコードも書けない 再現困難な バグのテストと検出に有効です このツールはバグの再現手順も 示してくれるので― 問題の理解と修正に大いに役立ちます
新しいC++のチェックで 検出できるのは― 移動元の使用に関するバグ C++のstd::stringで発生する ダングリングC文字列ポインタ DriverKitとIOKitでの 参照カウントのバグです 移動元の使用から説明します
C++ではmoveを使って 不要なコピーを回避できます その際 問題が起こりがちです 私のような冗長な人間が 小説を書いたとします 出版社に送る前に小説のテキスト全体を コピーするのは手間です そこでmoveを使います ソースの変数を コピーするのでなく移動します いい点としては 所有権に関する 一意のセマンティクスを強制できます 誰が最新版を所有しているかの 混乱が起きません 注意が必要なのはmoveを使うと ソースが未規定の状態になることです
具体的にお見せします
出版後にスペルチェック呼び出しを 追加したとします
これは本の型の実装によっては クラッシュにもつながります スタティックアナライザは このバグを検出します
修正にはコードの順序を変更します スペルチェックは 出版前に行うべきでしょう
次はstd::stringからの ダングリングポインタについてです
C++とCの文字列を混在させる時は いくらか工夫が必要です
generateGreetingという 関数があります C文字列のnameをとり C文字列のgreetingを返す関数です 実装には 扱いやすい C++のstd::stringを使うことにしました std::stringローカル変数を宣言して 初期値をHelloとし― 渡される名前を末尾に追加します この関数はC文字列を返すので― c strメソッドを C++文字列で呼び出します ここが問題の元です c strが返すのは― std::string内部のバッファを指す 内部ポインタです std::stringがスコープ外になると このバッファは解放されます
つまり破棄されるメモリへの ポインタを返しているのです このメモリの使用は クラッシュにもつながります
このバグもスタティックアナライザで 検出されます
お勧めの修正方法は― C++とCの文字列の存続期間を 一致させることです generateGreeting関数で std::stringを返すよう変更しました 結果はローカル変数に格納します
c strメソッドを呼び出す際― このローカル変数はC文字列を使う間 スコープ内にとどまります
つまりstd::stringのスコープを 必要な長さに変更したのです
できるだけC++にとどまるほうが 多くの場合 処理が簡単になります C文字列は必要な時だけ取得します
では最後のチェック機能に移りましょう DriverKitとIOKitの 参照カウントのバグについてです
これらのドライバフレームワークでは 手動の保持と解放を使って― メモリを管理します CoreFoundationやObjective-Cの― Automatic Reference Countingを 使わない方法とよく似ています 手動の保持と解放で 多くのメモリ管理作業を行えますが― その際は特別な注意が必要です
メモリを解放しすぎてはいけません メモリが破棄され 使用時に クラッシュが起こる可能性があります
逆に 解放しない場合も メモリリークの危険があります
リークの例を挙げましょう あるコードで新しいデバイス配列を 割り当てて― デバイスを代入し セットアップするとします このOSArray::withCapacityですが― 新しい配列を割り当て 保持状態で返しています つまり配列を解放しないと リークが起こります
このバグもアナライザで検出されます
修正方法としては 用が済んだ時に配列を解放するだけです
メモリ管理のルールはすべて 命名規則が中心になります CoreFoundationやObjective-Cの 手動の保持や解放と基本は同じです IOKitやDriverKitでの重要な違いは― デフォルトの規則が “保持して返す”という点です +1と呼ぶこともあります クライアントはメソッドを呼び出したら 結果を解放しないと― オブジェクトがリークします
重要な例外として ゲッターは結果を 解放状態つまり+0で返します ゲッターの結果を解放してはいけません
この規則に沿っていない コードの例をお見せしましょう
配列の最初のデバイスを 見つけるメソッドです デフォルトの規則に沿えば 結果を保持して返すはずです しかし実装を見ると― ゲッターとして 結果を解放して返しています これは問題です
アナライザではこの問題が検出されます
修正方法としては― 3種類あります 1つ目は規則に従うよう 動作を変える方法です メソッドで結果を保持して返すのが 規則なら 保持の処理を追加します
2つ目はメソッドの 名前を変える方法です findFirstDeviceメソッドは ゲッターの働きをするので― 名前を“getFirstDevice”に変えれば ガイドラインどおりです
メソッドの動作も名前も 変えたくないのであれば― 変えないでおくこともできます その場合はアノテーションを追加し― コードの読み手とアナライザに 意図的な指定だと伝えます
“DRIVERKIT RETURNS NOT RETAINED”と 追加すればいいでしょう
IOKitドライバでも 新しいDriverKitドライバでも― ぜひアナライザをご利用ください 実行にはXcodeの“Product”メニューで “Analyze”を選択します ビルドのたびに 解析を実行するのであれば― 対象のビルド設定で“Analyze During ‘Build’”を有効にします これでコミット前でも バグを検出できます
今日は多くのお話をしました まずLLVM BitcodeによるwatchOSの シームレスな64ビット変換 32ビットアプリケーションが Series 4 Watchで動いていました 次にコンパイラの新しい最適化機能と 言語によるコードサイズの削減 最後はスタティックアナライザの 実行方法です
詳しくはWWDCのWebサイトを ご覧ください ラボでもお待ちしています ありがとうございました (拍手)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。