
-
Swiftによるメモリ使用量とパフォーマンスの向上
Swiftコードのパフォーマンスとメモリ管理を向上させる方法を学びましょう。アルゴリズムの全体的な変更から、新しいInlineArrayおよびSpanタイプによるメモリと割り当ての詳細な制御まで、既存のコードをブラッシュアップする方法について説明します。
関連する章
- 0:00 - イントロダクションとアジェンダ
- 1:19 - QOIフォーマット&パーサアプリ
- 2:25 - アルゴリズム
- 8:17 - 割り当て
- 16:30 - 排他性
- 19:12 - スタック vs ヒープ
- 21:08 - 参照カウント
- 29:52 - Swiftバイナリ解析ライブラリ
- 31:03 - 次のステップ
リソース
関連ビデオ
WWDC25
WWDC24
-
このビデオを検索
こんにちは Swift Standard Library担当の Nate Cookです 本日は コードのパフォーマンスを確認して 改善する方法を説明するとともに Swift 6.2と その標準ライブラリに追加された 新機能を紹介します 新しいInlineArray型とSpan型を 実際に使用して 値ジェネリクスを試し エスケープ不可の型について学びましょう このような新しいツールを使用することで 保持と解放 排他性と一意性のチェック などの処理を省略できます また これらのツールを使ってバイナリ パーサの速度と安全性を高めるための 新しいオープンソースライブラリを 紹介します それがSwift Binary Parsingライブラリです このライブラリは速度を重視し 数種類の安全性を管理するための ツールを備えています またSwiftには 誰もが望むコード高速化の ためのツールが用意されています しかし 高速化が容易でない場合もあります このセッションでは コード内で実行に 時間がかかっている場所を特定して いくつかの方法で パフォーマンス最適化を試みます 適切なアルゴリズムの選択 不要な割り当ての排除 排他性チェックの排除 ヒープからスタックへの割り当ての移行 そして 参照カウントの削減です ここからは 私が作った 簡単なアプリの内容を見ていきます このアプリは QOIという形式の 画像を表示するビューアです この画像形式に対応する 自作パーサが含まれています QOIはロスレス画像形式です 仕様が1ページに収まるほどシンプルなので 様々なアプローチを試して そのパフォーマンスを確認する場合に 適しています QOI形式はバイナリ形式の 標準イディオムを使用します 固定サイズのヘッダに続く データセクションには 数が動的に変化する多様なサイズの エンコード済みピクセルが含まれます エンコード済みピクセルの形式としては RGB/RGBA値、 前のピクセルとの差異、 前に表示されたピクセルの キャッシュの参照、 前のピクセルの繰り返し回数があります では 私のQOIパーサアプリを 使ってみましょう
このアイコンファイルを開きます わずか数キロバイトなので すぐに読み込まれます
この鳥の写真は少しサイズが大きいです 読み込みに数秒かかる場合があります 読み込まれました なぜ時間がかかるのでしょうか 実際のデータで顕著な速度低下が見られる場合 アルゴリズムまたはデータ構造の 使用方法が誤っている 可能性が高いと言えます Instrumentsを使って この問題の原因を見つけて解決しましょう 私の解析ライブラリ内に 読み込みが遅かった鳥の画像を解析する テストを作成しました 実行ボタンをクリックして テストを実行すると
数秒後に問題なく終了します このテストを使った 正確性のチェックに加えて Instrumentsでテストをプロファイリングし パフォーマンスを確認することもできます 次に 実行ボタンを副ボタンで クリックします テストをプロファイリングするための オプションがあります
とても便利な機能で テストをプロファイリングする際 コード内の興味のある部分に 焦点を当てることができます このオプションをクリックして Instrumentsを起動します
Instrumentsのテンプレート選択ダイアログに コードのパフォーマンスを 把握するために役立つ さまざまなテンプレートが表示されます 今回は2種類のinstrumentを使用するので 空白のテンプレートで開始しましょう
instrument追加ボタンをクリックして instrumentを追加します パーサによるメモリの 割り当て状況を調べたいので Allocations instrumentを追加します また 時間がかかっている 場所を知りたいので Time Profiler instrumentを追加します
Time Profilerはパフォーマンスの問題を 調べる際に最初に使用するべき機能です サイドバーを非表示にして 結果が大きく表示されるようにしましょう 次に記録ボタンをクリックして テストを開始します
結果ウインドウに 様々なデータが表示されます
プロファイルに含まれるinstrumentは ウインドウの上部に一覧表示されます まず「Time Profiler」を使用するので これを選択しておきます 下部には選択中のツールの 詳細ビューが表示されます 左側には キャプチャされた コールがリスト表示され 右側には 現在選択中のコールの 高負荷のスタックトレースが表示されます
まず 到達方法にかかわらず 最も頻繁にキャプチャされた コールを確認しましょう ボタンをクリックして
チェックボックスを選択します グラフィカルビューに切り替えるには 詳細ビューの 上部のボタンをクリックします ビューが切り替わり プロファイルの フレームグラフが表示されます
フレームグラフの各バーは プロファイリング中に 各コールがキャプチャされた回数の 割合を示しています これを見ると「platform_memmove」 と表記されたバーが プロセスの大部分を占めています 同じシンボルがスタックトレースにもあります memmoveはデータをコピーする システムコールなので この巨大なバーは パーサがほとんどの時間を データの読み取りよりも コピーに費やしていることを示しています これは望ましくない動作です コードのどの部分がこのコピー動作を 引き起こしているか調べてみましょう スタックトレースにすべてのフレームを 表示するため ビューの上部にある ボタンをクリックします
トレースの一番上にシステムコールが表示され platform_memmoveも含まれています また Foundation Data型の 特殊なバージョンの メソッドもいくつか表示されています この特殊なメソッドは スタックトレース またはデバッグ中に目にすることもあります この特殊なメソッドは Swiftコンパイラによって生成された 型固有バージョンのジェネリックコードです
こちらが私が定義したメソッドである readByteです
コード内で問題に 最も近い関数がこれですので ここから始めるのがよいでしょう このメソッドにジャンプするため 副ボタンでクリックして を選択します
Xcodeのこの部分で readByteメソッドが宣言されています この行に自動的に移動します ここで最初のバイトをドロップして データイニシャライザを呼び出しています Instrumentsを使用することで ライブラリの速度低下の原因の可能性がある すべてのmemmoveコールを特定し 大量のコピー処理を発生させている コード行にジャンプすることができました
このヘルパメソッドは本当に重要です 私の解析コードは readByteを何度も呼び出しながら Rawバイナリデータを消費しているからです
readByteメソッドを呼び出すたびに 最初のバイトを返して データの開始を前方に移動するため データが縮小すると考えていました しかし実際は 1バイト読み取るたびに データのコンテンツ全体を 新しい割り当て先にコピーします 考えていたよりもはるかに重い処理でした この間違いを正しましょう Xcodeに戻って readByteメソッドを編集します
Data型は両端から縮小する仕様なので 「popFirst()」という コレクションメソッドを利用できます popFirst()は データの最初のバイトを返した後 コレクションの冒頭部分を 前方にスライドさせ 1バイトだけ縮小させます 私が求めていた動作です
これを修正したらテストに戻り プロファイルを再度実行します
Instrumentsが自動的に開きます 同じプロファイリング構成で 既にテストが実行されています 問題ありませんね platform_memmoveの 巨大なバーがフレームグラフから消えました
ベンチマークを実行すると 変更の結果として実行速度が 大幅に改善されたのがわかります 素晴らしい結果ですが このようなアルゴリズム変更がもたらすのは こうした明白な変化だけではありません 元のバージョンでは 画像のサイズとその解析に要する時間の 関係性は二次関数的なものでした 解析する画像が大きいほど 解析にかかる時間が 大幅に長くなりました コピーの問題を修正したことで この関係が線形的になります 画像のサイズと その解析時間の関係性は もっと直接的なものになります ここから さらに修正を加えて この線形パフォーマンスを改善することで それぞれの修正方法を 直接比較できるようになります
この問題はいったん置いておいて パフォーマンス悪化の主要原因の2つ目 過剰な割り当てに注目しましょう 現在 最も重いスタックトレースは どれでしょうか これらのコールは Swift配列の 割り当て/割り当て解除を行う メソッドへのトラフィックが 多いことを示しています メモリの割り当て/割り当て解除は 高コストなので この過剰な割り当ての原因を突き止めて それを解消できれば パーサを高速化できるでしょう パーサが実行している 割り当てを確認するために 先ほど追加した Allocations instrumentを使用します いくつかの指標により このコードが 不必要な割り当てを 行っていることがわかります 第1は 数が多すぎることです 1つの画像の解析で 100万回近くの 割り当てが行われています もっと効率化できるはずです 第2に これらのほぼすべてが 一時的な割り当てであることがわかります Allocations instrumentにより 一時的なものとしてマークされています
問題の原因を見つけるために 詳細パネルを コールツリービューに切り替えましょう ポップアップボタン をクリックして を選択します
一番上のスレッドを選択し スタックトレースで この問題に最も近い コード内の領域を探します このスタックトレースは反転していないので トレースの一覧の一番下を 見る必要があります 私のパーサの最初のシンボルは このRGBAPixel.dataメソッドです
このメソッドをクリックすると コール ツリーの詳細ウインドウに表示されます このメソッドを副ボタンでクリックし を選択して ソースに直接ジャンプします
このメソッドが 過剰な割り当ての原因のようです このメソッドは 呼び出されるたびに ピクセルのRGB値または RGBA値が含まれる 配列を返します つまり 呼び出されるたびに配列を作成し 3つ以上の要素を 格納できるスペースを割り当てます
どこで使用されているかを調べるには この関数名を副ボタンでクリックして を選択します
呼び出し元は メインの解析関数にある このクロージャです これはこの大きなflatMapと prefixチェーンの一部にすぎません このコードが膨大な数の個別割り当てを 行う理由を理解するために 割り当てがどのように重なっていくのか 段階的に見ていきましょう
まずreadEncodedPixelsメソッドが バイナリデータをピクセルにエンコードします 先ほど述べたようにこれは 多様なピクセルタイプであり これを格納できるスペースを 割り当てる必要があります
次に エンコードされたピクセルごとに decodePixelsが呼び出され 1つ以上のRGBAピクセルを生成します ほとんどのエンコーディングでは 1ピクセルのみが出力されますが 1つのエンコーディングが前のピクセルを 一定回数繰り返すよう指定しています これをサポートするためにdecodePixelsは 常に配列を返します この配列のそれぞれを 割り当てる必要があります
flatMapの「フラット化」部分が 先ほど作成された小さな配列を すべて受け取ってマージし 1つのはるかに大きな配列を作成します これは新しい割り当てです 先ほど作成した小さな配列は すべて割り当て解除されます
このprefixメソッドによって 生成できるピクセル数に 上限が設定されています
2つ目のflatMapは まず Allocations instrumentを使用した時に フラグが付けられた RGBAPixel.dataメソッドを呼び出します 先ほど このメソッドが3要素または4要素を 含む配列を返すことを確認しました これを見ると 最終的な画像の 1ピクセルごとに3要素または4要素の 配列が1つずつ生成されることがわかります コンパイラによってこの過剰な割り当てを 一部最適化できる可能性がありますが トレースで見たように それが常に行われるとは限りません
次に 小さな配列群が1つの大きな 新しい配列に再びフラット化されます 最後にRGBピクセルまたは RGBAピクセルのデータの大きな配列が 新しいDataインスタンスにコピーされ 返すことが可能になります
このコードにはある種の 洗練された美しさがあります 短いメソッド呼び出しの連鎖に 多くのパワーが詰め込まれています しかし短いからといって 速いわけではありません こうした様々な手順を処理して 最終的にDataインスタンスを返すのではなく 最初にデータを割り当てた後で バイナリソースデータからデコードする際に 各ピクセルを書き込めば まったく同じ処理を実行しながらも こうした中間的な割り当てが 一切不要になります 先ほどの解析関数に戻りましょう このメソッドを修正して 余分な割り当てをすべて排除します
まず 「totalBytes」 つまり結果データの最終的な サイズを計算します 次に「pixelData」に 適切なサイズのストレージを割り当てます 「offset」変数は 書き込まれたデータの量を追跡します この事前割り当てにより バイナリデータを扱う過程で 追加の割り当てが不要になります
次に 各データを解析して ただちに処理します switchステートメントを使用して 解析済みピクセルを処理できます
実行を指示するエンコード済み ピクセルについては 必要な回数だけループさせて 毎回ピクセルデータを書き出します
他の種類のピクセルについては デコードしてデータに直接書き込みます これは完全な書き換えであり 返す必要がある データを除いて割り当てを行いません テストを再度プロファイリングして 問題が修正されたことを確認しましょう
割り当ての数が大幅に 減ったことがすぐにわかります コード内の実際の割り当て数を 確認するためにフィルタを使用します ウインドウの下部にある フィルタフィールドをクリックして 「QOI.init」と入力します
これによりスタックトレースに QOI.initを含まない コールツリーが除外されます 残った行を見ると パーサコードが 少数の割り当てしか実行せず 合計で2MBもないことがわかります Optionキーを押しながら三角印を クリックするとコールツリーが展開されます
展開されたツリーに必要な情報があります
割り当てが実行されるのは 結果の画像が格納されるDataのみです
ベンチマークを見ると 大きく改善されていることがわかります 過剰な割り当てを排除することで 実行時間が半分以下に短縮されました
ここまで パーサのアルゴリズムに2つの 変更を加えることで 大量の誤コピーを解消し 割り当て処理の数を減らしました 以降のいくつかの改善では さらに高度な手法を使って 実行時に行われる 自動メモリ管理処理の多くを Swiftコンパイラによって 解消します
まず 配列やその他のコレクション型が どのように機能するかについて説明します SwiftのArray型は 処理速度が速く安全で使いやすいため Swiftで最も人気のあるツールの1つです 配列は必要に応じて拡大縮小できるため 事前に作業対象のアイテム数を 把握しておく必要がありません 背後でSwiftが自動的に メモリを処理します 配列も値型です つまり配列のコピーに 加えた変更は 他のコピーに影響を与えません 配列のコピーを作成する場合は 配列を別の変数に割り当てるか 関数に渡します Swiftでは要素が即座に 複製されることはありません その代わりにコピーオンライトと呼ばれる 最適化手法が使用されます これにより いずれかの配列を 実際に変更するまで複製が遅延されます
このような機能を備えた配列は 優れた汎用コレクションですが トレードオフもいくつかあります 動的なサイズ変更と 複数の参照をサポートするため 配列のコンテンツは 多くの場合 ヒープ上の別の割り当てに格納されます Swiftランタイムは参照カウントを使用して 各アレイのコピー数をトラッキングします 変更が加えられると 配列が一意性をチェックし 要素を コピーする必要があるかどうか確認します さらに Swiftはコードを安全に保つために 排他性を適用します つまり 2つの異なるものが 同じデータを同時に 変更できないようにします この規則は多くの場合コンパイル時に 適用されますが 実行時にのみ適用される場合もあります 低レベルの概念について 学習したところで 次は プロファイリングでどのように 表示されるかを見ていきます まず 排他性の ランタイムチェックについてです これによってプログラムの変更が必要になり 最適化が妨げられる可能性があります これから排他性チェックについて 説明するわけですが ここに良い例題があります パフォーマンスを改善したことで Instrumentsがパーサプロセスを確認する 十分な時間がなくなりました 解析コードをループすることで 確認する時間を少し増やすことができます 50回もあればよいでしょう
改善したプロファイルを見てみましょう
排他性テストはトレース内で 「swift_beginAccess」シンボルと 「swiftendAccess」シンボルで表されます もう一度ウインドウの下部にある フィルタボックスをクリックして シンボル名を入力します
フレームグラフの上部に swift_beginAccessが数回現れており そのすぐ下にこのチェックを 必要とするシンボルがあります このシンボルは 直前のピクセルと ピクセルキャッシュのアクセサであり 私のパーサの Stateクラスに格納されています Xcodeに戻って その宣言を探してみましょう ここですね Stateはフレームグラフに表示されていた 2つのプロパティを持つクラスです Classインスタンスの変更は Swiftが実行時に排他性を チェックする必要がある状況の1つなので このような結果になった原因は この宣言にあります このチェックを排除するには これらのプロパティをクラスの外に移動し それを直接パーサ型に入れます
次に検索置換を実行して previousPixelとpixelCacheへの 「state.」アクセスを削除します
ビルドすると もう少し修正が 必要なことをコンパイラが知らせてくれます
stateプロパティが クラス内でネストされなくなるため イミュータブルのメソッドでは これを変更できません
この修正を適用して メソッドをミュータブルにします
修正はもう1か所あります
これで完了です 変更が終わりましたので テストに戻りましょう
プロファイルを再記録して 変更の結果を確認します
再びswift_beginAccessに フィルタをかけます
何も表示されません これで実行時の排他性チェックが 完全に削除されました 状態変数をもう一度見てみましょう ここで Swiftの新機能を使用して ヒープメモリからスタックメモリに データを移動し 排他性チェックが入る余地をなくしましょう このパーサのピクセルキャッシュは RGBAPixelの配列です 64個の要素を持つよう初期化され サイズは変更されません このキャッシュは新しいInlineArray型を 使用するのに最適です InlineArrayはSwift 6.2の 新しい標準ライブラリ型です 通常の配列と同様 連続するメモリ内に 同じ種類の要素を格納します しかし大きな違いがいくつかあります まず InlineArrayはコンパイル時に 固定サイズが設定されます 通常の配列では追加や削除が可能ですが InlineArrayは 新しい値ジェネリクス機能を使用して そのサイズを型の一部にします つまり InlineArrayの要素に 変更を加えることはできますが サイズが異なる配列へのInlineArrayの 追加、削除、割り当てはできません
次に その名前が示すように InlineArrayを使用すると 要素が常にインラインで格納され 個別に割り当てられません InlineArrayはコピー間で ストレージを共有せず コピーオンライトを使用しません 代わりに コピーを作成するたびに コピーされます そのため 通常の配列で必要となる 参照カウント、一意性チェック、 排他性チェックが不要です InlineArrayのコピー動作が 異なることは ある意味で 諸刃の剣となります 配列を使用するために 異なる変数間またはクラス間で コピーの作成または参照の共有が必要な場合 InlineArrayは適切でない可能性があります ただしこの場合 ピクセルキャッシュは 固定サイズの配列であり 変更が適用されますが 決してコピーされません 「InlineArray」が適している場所ですね
最後の最適化では 標準ライブラリの 新しいSpan型を使用して 解析中に発生する参照カウントの 大部分を排除します Time Profilerのフレームグラフに戻って もう一度フィルタを適用し QOIパーサのみを見てみましょう フィルタボックスに 「QOI.init」と入力します
解析イニシャライザを含む スタックトレースのみが ビューに表示されます retainシンボルとreleaseシンボルに 注目しましょう このピンク色のバーがswift_retainで 7%のサンプルに出現しています これがswift_releaseで これも7%に出現します 先ほどお話しした 一意性チェックはここにあります 3%のサンプルに出現しています
これがどこから来ているのか調べるために swift_releaseに戻ります これまでと同様 最も負荷の高い スタックトレースをスキャンして 最初のユーザ定義メソッドを見つけます 最初に使用したものと同じ readByteメソッドのようです
今回扱うのはアルゴリズムの問題ではなく 「Data」自体の使用方法です 「Array」と同様に「Data」も通常は メモリをヒープに格納し 参照カウントの必要があります このような参照カウント処理 つまり保持と解放は非常に効率的ですが それが短いループで発生すると 膨大な時間が消費される場合があります それがこのメソッドの問題です これに対処するには Dataや Arrayなどの高レベルのコレクション型から 参照カウントの爆発的増加を起こさない型に 処理を移行する必要があります Swift 6.2までは withUnsafeBufferPointerなどの メソッドを使用してコレクションの 基盤ストレージにアクセスしていました これらのメソッドを使用すると メモリを手動で管理でき 参照カウントが発生しません ただし コードの安全性は損なわれます
ポインタが安全でない理由を 考えてみましょう ポインタはSwiftの安全性保証の 多くを回避するため 安全でない状態で呼び出されます ポインタは初期化メモリと 未初期化メモリをどちらもポイントでき いくつかの型の保証を適用せず コンテキストからエスケープできるため 割り当てられていないメモリに アクセスするリスクが生じます 安全でないポインタを使用する場合 コードの安全性を確保する責任は デベロッパ自身が負うことになります コンパイラは助けてくれません このprocessUsingBuffer関数は 安全でないポインタを適切に扱っています 使用範囲全体が安全でない バッファポインタクロージャ内に収まり 計算の結果のみが最後に返されます これに対して この「getPointerToBytes()」 関数は危険です これには重大なエラーが2つ含まれています この関数はバイトの配列を作成し UnsafeBufferPointerメソッドを 使って呼び出しを行います しかしクロージャへのポインタの 使用を制限せずに 外側スコープへのポインタを返します エラーその1 さらに悪いことに 次にこのコードは この関数から有効でなくなった ポインタを返します エラーその2 2つのエラーによって ポインタの有効時間が ポイント先の有効時間を超えて 延長されるため 移動または割り当て解除されたメモリへの 危険な参照が残されることになります これに対処するため Swift 6.2にはSpanと呼ばれる 新しい型のグループが導入されました Spanはコレクションに属する 連続メモリを扱うための 新しい手段となります Spanの新しい「エスケープ不可」 言語機能により コンパイラは 連続メモリの有効時間を 提供元のコレクションに 関連付けることができます Spanがアクセスを提供するメモリは そのSpanの間だけ 存続することが保証されるため 参照が残存する可能性がなくなります すべてのSpan型は エスケープ不可として宣言されるため コンパイラではエスケープを実行できず 取得元のコンテキストの外部に Spanを返すこともできません この「processUsingSpan」メソッドのように ポインタよりもシンプルで 安全なコードをSpanによって作成できます Spanに配列の要素を含めるには spanプロパティを使用します クロージャを使わずに 危険なポインタと同じくらい効率的に 配列のストレージにアクセスでき 危険性を排除できます 以前の危険な機能を修正しようとすると 「エスケープ不可」言語機能が 動作しているのがわかります 最初にわかるのは この「Span」を含む同じ関数シグネチャを 作成することもできないことです Spanの有効期間は 提供元のコレクションに関連付けられるため コレクションまたはSpanが 渡されなければ 渡されるSpanの有効期間を 取得することができません
Spanをクロージャでキャプチャして コンパイラからSpanを隠してみましょう この関数では 配列を作成してSpanにアクセスし そのSpanをキャプチャするクロージャを 返そうとしています しかし それもうまくいきません Spanをキャプチャすると エスケープが可能になるため コンパイラは その有効時間が ローカル配列に依存することを指摘します Spanがスコープをエスケープしない このコンパイラチェック済み要件は 保持と解放が 必須ではないことを意味します 安全でないバッファを一切の危険なく 活用できるようになります Spanファミリには読み取り専用かつ ミュータブルなSpanの型付きバージョンと Rawバージョンが含まれ 既存のコレクションを扱うことができます 新しいコレクションの 初期化に使用できる出力Spanもあります このファミリには安全で効率的な Unicode処理のための新しい型である UTF8Spanも含まれています
コードに戻って RawSpanに このreadByteメソッドを実装しましょう
まずRawSpanのextensionを追加し
readByteメソッドを定義します
このRawSpanのAPIはDataとは少し 異なりますが 先ほど実装したものと 同じ処理を行います 最初のバイトをロードして RawSpanを縮小した後 ロードされた値を返します このunsafeLoadメソッドは 特定の種類の型をロードすると 危険である可能性があるため この方法でのみ命名してください 組み込み整数型はこのようにして ロードすると常に安全です
次に解析メソッドを更新しましょう
この2つの解析メソッドでは パラメータとしてDataではなく RawSpanを使用する必要があります
また呼び出し側にも 変更を加える必要があります
データそのものを渡すのではなく データのRawSpanを取得して それを解析メソッドに渡します DataのRawSpanには bytesプロパティを使ってアクセスします このrawBytes値はエスケープ不可です この関数からこれを 返すことはできませんが 問題なくこれを 解析メソッドに渡すことができます
この変更でRawSpanを使用するための 更新が完了しました 細かな作業をもっと省くために 解析メソッドに新しいOutputSpanを 導入することもできます
0に初期化された データを作成するのではなく 新しいrawCapacity イニシャライザに含まれる OutputSpanを使用して 未初期化データに段階的に入力します
OutputSpanは 書き込まれたデータの量を追跡するため そのcountプロパティを独立した offset変数の代わりに使用できます
さらにwrite(to:)メソッドの 別のバリエーションを使用し Dataインスタンスではなく outputSpanに書き込みます
では このメソッドの実装を確認しましょう
write(to:)メソッドはピクセルの各チャネル ごとにOutputSpanのappendメソッドを 呼び出すことができます OutputSpanはこのような用途を想定した エスケープ不可の型であるため 「Data」インスタンスに書き込むよりも シンプルかつ効率的で 安全でないバッファポインタに ドロップダウンするよりも安全です 以上の変更が完了したら テストに戻って新しいプロファイルを 記録しましょう
QOI.initでフィルタリングします
フレームグラフを見ると swift_retainと swift_releaseのブロックがなくなりました 良い結果が得られました ここで InlineArrayとRawSpanを 導入した結果を見てみましょう
今回加えた変更によって 安全でないコードに頼ることなく メモリ管理の修正によって 解析時間を6分の1に短縮できました これは二次関数的アルゴリズムの 除去後と比較して16倍高速であり 当初の状態と比較すると 700倍以上高速化されています 今回は様々な話題を取り上げました この画像解析ライブラリを修正するために アルゴリズムに2つの変更を加え 動作の効率性を高め 割り当て数を減らしました 実行時のメモリ管理を解消するために 新しい標準ライブラリ型である InlineArrayとRawSpanを使用しました そして新しいエスケープ不可 言語機能について学びました 新しいSwiftバイナリ解析ライブラリは この同じ機能の上に構築されています デベロッパはこのライブラリを使って バイナリ形式の安全で効率的な パーサを開発し 様々な種類の安全性を扱うことができます このライブラリは解析イニシャライザを含む 包括的なツールセットであり Rawバイナリデータの値を安全に 利用するために役立ちます
こちらは新しいライブラリを 使用して作成された QOIヘッダ用のパーサの例です これはその機能の一部を示しています ParserSpanは バイナリデータを解析するために カスタマイズされたRawスパン型です また解析イニシャライザによって 整数オーバーフローを防ぎ 符号属性、ビット幅、バイト順を 指定できます このライブラリには カスタムRaw表現可能型のための 検証用パーサと 信頼できない新たな解析値を 使って安全に計算を行うための オプション生成オペレータも含まれます
Apple社内では既に バイナリ解析ライブラリが使用されており 今日から一般公開されます ぜひ一度ご覧になり お試しください Swiftフォーラムに投稿するか GitHubの open issueまたはpull requestから コミュニティにご参加ください Swiftコードの最適化について ご視聴いただきありがとうございました XcodeとInstrumentsを使って アプリで高性能が求められる 部分のテストをプロファイリングしましょう 新しいInlineArray型とSpan型に ついてはドキュメントをご覧になるか 新バージョンの Xcodeをダウンロードしてください WWDCをお楽しみください
-
-
7:01 - Corrected Data.readByte() method
import Foundation extension Data { /// Consume a single byte from the start of this data. mutating func readByte() -> UInt8? { guard !isEmpty else { return nil } return self.popFirst() } }
-
9:56 - RGBAPixel.data(channels:) method
extension RGBAPixel { /// Returns the RGB or RGBA values for this pixel, as specified /// by the given channels information. func data(channels: QOI.Channels) -> some Collection<UInt8> { switch channels { case .rgb: [r, g, b] case .rgba: [r, g, b, a] } } }
-
10:21 - Original QOIParser.parseQOI(from:) method
extension QOIParser { /// Parses an image from the given QOI data. func parseQOI(from input: inout Data) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let pixels = readEncodedPixels(from: &input) .flatMap { decodePixels(from: $0) } .prefix(header.pixelCount) .flatMap { $0.data(channels: header.channels) } return QOI(header: header, data: Data(pixels)) } }
-
12:53 - Revised QOIParser.parseQOI(from:) method
extension QOIParser { /// Parses an image from the given QOI data. func parseQOI(from input: inout Data) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let totalBytes = header.pixelCount * Int(header.channels.rawValue) var pixelData = Data(repeating: 0, count: totalBytes) var offset = 0 while offset < totalBytes { guard let nextPixel = parsePixel(from: &input) else { break } switch nextPixel { case .run(let count): for _ in 0..<count { state.previousPixel .write(to: &pixelData, at: &offset, channels: header.channels) } default: decodeSinglePixel(from: nextPixel) .write(to: &pixelData, at: &offset, channels: header.channels) } } return QOI(header: header, data: pixelData) } }
-
15:07 - Array behavior
var array = [1, 2, 3] array.append(4) array.removeFirst() // array == [2, 3, 4] var copy = array copy[0] = 10 // copy happens on mutation // array == [2, 3, 4] // copy == [10, 3, 4]
-
19:47 - InlineArray behavior (part 1)
var array: InlineArray<3, Int> = [1, 2, 3] array[0] = 4 // array == [4, 2, 3] // Can't append or remove elements array.append(4) // error: Value of type 'InlineArray<3, Int>' has no member 'append' // Can only assign to a same-sized inline array let bigger: InlineArray<6, Int> = array // error: Cannot assign value of type 'InlineArray<3, Int>' to type 'InlineArray<6, Int>'
-
20:23 - InlineArray behavior (part 2)
var array: InlineArray<3, Int> = [1, 2, 3] array[0] = 4 // array == [4, 2, 3] var copy = array // copy happens on assignment for i in copy.indices { copy[i] += 10 } // array == [4, 2, 3] // copy == [14, 12, 13]
-
23:13 - processUsingBuffer() function
// Safe usage of a buffer pointer func processUsingBuffer(_ array: [Int]) -> Int { array.withUnsafeBufferPointer { buffer in var result = 0 for i in 0..<buffer.count { result += calculate(using: buffer, at: i) } return result } }
-
23:34 - Dangerous getPointerToBytes() function
// Dangerous - DO NOT USE! func getPointerToBytes() -> UnsafePointer<UInt8> { let array: [UInt8] = Array(repeating: 0, count: 128) // DANGER: The next line escapes a pointer let pointer = array.withUnsafeBufferPointer { $0.baseAddress! } // DANGER: The next line returns the escaped pointer return pointer }
-
24:46 - processUsingSpan() function
// Safe usage of a span @available(macOS 16.0, *) func processUsingSpan(_ array: [Int]) -> Int { let intSpan = array.span var result = 0 for i in 0..<intSpan.count { result += calculate(using: intSpan, at: i) } return result }
-
25:07 - getHiddenSpanOfBytes() function (attempt 1)
@available(macOS 16.0, *) func getHiddenSpanOfBytes() -> Span<UInt8> { } // error: Cannot infer lifetime dependence...
-
25:28 - getHiddenSpanOfBytes() function (attempt 2)
@available(macOS 16.0, *) func getHiddenSpanOfBytes() -> () -> Int { let array: [UInt8] = Array(repeating: 0, count: 128) let span = array.span return { span.count } }
-
26:27 - RawSpan.readByte() method
@available(macOS 16.0, *) extension RawSpan { mutating func readByte() -> UInt8? { guard !isEmpty else { return nil } let value = unsafeLoadUnaligned(as: UInt8.self) self = self._extracting(droppingFirst: 1) return value } }
-
28:02 - Final QOIParser.parseQOI(from:) method
/// Parses an image from the given QOI data. mutating func parseQOI(from input: inout RawSpan) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let totalBytes = header.pixelCount * Int(header.channels.rawValue) let pixelData = Data(rawCapacity: totalBytes) { outputSpan in while outputSpan.count < totalBytes { guard let nextPixel = parsePixel(from: &input) else { break } switch nextPixel { case .run(let count): for _ in 0..<count { previousPixel .write(to: &outputSpan, channels: header.channels) } default: decodeSinglePixel(from: nextPixel) .write(to: &outputSpan, channels: header.channels) } } } return QOI(header: header, data: pixelData) }
-
28:31 - RGBAPixel.write(to:channels:) method
@available(macOS 16.0, *) extension RGBAPixel { /// Writes this pixel's RGB or RGBA data into the given output span. @lifetime(&output) func write(to output: inout OutputRawSpan, channels: QOI.Channels) { output.append(r) output.append(g) output.append(b) if channels == .rgba { output.append(a) } } }
-