ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
ヒープメモリの分析
アプリのダイナミックメモリであるヒープの基本を解説します。InstrumentsとXcodeを使用して、ヒープに関する一般的な問題を測定、分析、修正する方法を確認しましょう。一時的な増加、永続的な増加、リークをアプリで診断するための手法とベストプラクティスもご紹介します。
関連する章
- 0:00 - Introduction
- 1:05 - Heap memory overview
- 3:45 - Tools for inspecting heap memory issues
- 7:40 - Transient memory growth overview
- 10:34 - Managing autorelease pool growth in Swift
- 13:57 - Persistent memory growth overview
- 16:00 - How the Xcode memory graph debugger works
- 20:15 - Reachability and ensuring memory is deallocated appropriately
- 21:54 - Resolving leaks of Swift closure contexts
- 24:13 - Leaks FAQ
- 26:51 - Comparing performance of weak and unowned
- 30:44 - Reducing reference counting overhead
- 32:06 - Cost of measurement
- 32:30 - Wrap up
リソース
関連ビデオ
WWDC24
WWDC22
WWDC21
WWDC18
-
ダウンロード
「ヒープメモリの分析」へようこそ こちらがBenです そしてこちらがDanielです 今日はヒープメモリと アプリについてお話しします ヒープメモリは アプリによって直接的 および間接的に使用され デベロッパとしてそれを 制御し 最適化することができます ヒープメモリは アプリの参照型が格納される場所であり 通常は書き込みによって ダーティになるため アプリのメモリ制限に影響する 重要なものです そのため本セッションではヒープメモリの 測定と削減に焦点を当てます
グラフィックメモリやメモリ制限など ほかの種類のメモリについて 詳しく知りたい場合は これらを詳しく解説している ほかのセッションをご覧ください アプリがメモリを使いすぎている場合や 私のように好奇心で 内容を確認したいという方も ぜひご覧ください 本セッションでは5つのトピックを扱います ヒープの測定 一時的な使用量増加への対処 持続的な使用量増加の追跡 メモリリークの修正 そして 実行時のパフォーマンスの向上です 最初のトピックは ヒープメモリの概要と アプリで使用中のヒープメモリを 測定するためのツールについてです ヒープを理解するには アプリの仮想メモリ全体で どこに位置するのかを知る必要があります アプリは起動時に 仮想メモリ内に 空のアドレス空間を取得します システムはアプリ起動時に メインの実行可能ファイルと リンクされたライブラリ フレームワークをロードし ディスクから 読み取り専用リソースの領域をマップします 実行時 アプリはスタック領域も使用して その領域に 各スレッドのローカル変数と 一時変数を格納し 長期間存続する動的なメモリを 複数のメモリ領域に配置します これらの領域を総称してヒープと呼びます 本セッションではこの領域に注目します 拡大しましょう ヒープは1つのメモリブロックではなく 複数の仮想メモリ領域で構成されています もう少し拡大し 各領域レベルで見てみます 各領域は個別のヒープの割り当てに 分割されます 詳細に見ると これらの領域は オペレーティングシステムの 16KBのメモリページで構成されています ただし 各割り当てのサイズは増減します これらのメモリページの状態は3つあり クリーン ダーティ スワップ済みの いずれかです クリーンページは まだ書き込みのないメモリです 割り当て済みであるけれど未使用の領域や ディスクから読み取り専用でマップされた ファイルが含まれます これらのページは システムがいつでも破棄し 再度フォールトできるため 低コストです ダーティページは アプリによって 最近書き込まれたメモリです ダーティページは しばらく 使われていなくても破棄できません メモリ不足になると システムはこれらをスワップし 圧縮するか ディスクに書き込みます これにより 必要な時に メモリを解凍するか ディスクからフォールトできます 3種類のページのうち ダーティとスワップ済みのみが アプリの メモリフットプリントにカウントされ ほとんどのアプリでは メモリフットプリントの大部分を ヒープが占めます
ヒープ領域は 関数mallocや それと類似する 割り当てプリミティブであるcallocや reallocにより作成されるメモリです 多くの場合 これらの関数は直接呼び出しせず コンパイラとランタイムにおいて SwiftやObjective-Cクラスの インスタンス作成などによく使用されます アプリはmallocにより 存続期間の長い メモリを動的に割り当てできます 割り当ては明示的に解放されるまで 存続します つまり これらを作成したコードの スコープの終了後も存続します これらの関数は数個のルールを適用します 例えば 割り当てサイズと アライメントの最小値は16バイトです そのため 4バイトを指定すると 16に切り上げられます またセキュリティ機能として ほとんどの 小さい割り当ては解放時にゼロになります 言語のランタイムはヒープにより 存続期間の長いメモリを割り当てます 例えば Swiftは このクラスイニシャライザを展開し 一連のSwiftランタイム関数を呼び出します その最後にmallocが呼び出されます
また mallocにはデバッグ機能があります その1つがMallocStackLoggingであり 各割り当てのコールスタックと タイムスタンプを記録します MallocStackLoggingを有効にすると メモリを割り当てた 場所と時点の追跡が 大幅に容易になります
Xcodeでは スキームの タブのチェックボックスで MallocStackLoggingを有効にできます mallocのスタックログ作成を 有効にしたので 本セッションの全デモで利用できます メモリの使用状況を 追跡するための1つ目のツールは Xcodeのメモリレポートです アプリのフットプリントを 経時的に示すツールです アプリのフットプリントは ヒープだけではありませんが メモリレポートでは メモリに関する 主な問題と最近の履歴を把握できます 残念ながら メモリ使用量の 増加の理由は表示されません これを調査するには別のツールが必要です 今回ご紹介するもう1つのツールも Xcodeに含まれています メモリグラフデバッガは メモリグラフを取得します これは すべての割り当てと それらの間の参照のスナップショットです MallocStackLoggingを使えば 各割り当てのバックトレースが得られます 特定の割り当てに注目する場合 メモリグラフデバッガは最適であり Xcodeのデバッグバーから 直接アクセスできます
Xcodeにはメモリ分析用の 強力なコマンドラインツールもあります leaks、heap、vmmap、malloc_historyは macOSとSimulatorのプロセスの 直接の分析や 取得済みのメモリグラフによる 問題の調査に使用できます これらのツールのmanページを参照して 高度な機能を確認することをおすすめします
メモリ使用量を経時的に プロファイルする場合 Instrumentsアプリでは 数種のテンプレートを利用できます 「Allocations」instrumentは 割り当てと解放の全イベントの履歴を 経時的に記録し 統計とコールツリーを集約するため コードをさかのぼる際に役立ちます 「Leaks」instrumentはアプリのメモリの スナップショットを定期的に取得し メモリリークを検出します 次に サンプルアプリである Destination Videoの問題を 「Allocations」で調査する方法を確認します
新しい機能にメモリの問題があります この機能はDanielと私が このアプリ用に開発しました この機能では ビデオの 新しい背景画像を選択できます 何度か背景画像のギャラリーを 開いたり閉じたりすると アプリがクラッシュしました 大量のメモリを消費したためです Xcodeのメモリレポートによると ギャラリーを開くたびに メモリ使用量が約1GBに急増しています これを「Allocations」instrumentで 分析できます 私のデバイスで試してみます
Instrumentsでのプロファイル作成は > から行います アプリのリリースビルドを実行し それをターゲットに選択して Instrumentsを開きます 開く時に プロファイリング用の テンプレートの選択を求められます この例ではヒープのプロファイルを 作成するので を選択します 「Allocations」と「VM Tracker」の2つの instrumentがこのテンプレートに含まれます 「Allocations」は ヒープとVMイベントをライブで記録し 動きをリアルタイムで確認できるようにします 「VM Tracker」ではスナップショットを 定期的に取得し 全仮想メモリを測定できます この例ではヒープだけに注目するので これは利用しません トレースは トレースドキュメントの左上の ボタンで開始します トレースが始まると アプリのデータストリーミングが始まります アプリは読み込み済みのため トラックビューでは メモリ使用量は一定です ギャラリービューを開いて閉じ 使用量の急増時に 何が起きているか確認します
前回よりも少し遅くなりますが これは予想通りです mallocとfreeすべての スタックトレースを取得します このデータはこの後すぐに 非常に役に立ちます
左上の停止ボタンをクリックして トレースを止めます 「Allocations」トラックを見ると スパイクパターンが再現されています トレースファイルができたので メモリバグが大好きな パートナーに送りましょう メニューで > を選択し トレースを保存します ここからはDanielの出番です
一時的なメモリ増加の診断について 説明してもらえますか もちろんです Benが記録したメモリスパイクの詳細を見て 対策を考えてみましょう アプリに現れるメモリスパイクは 一時的なメモリ増加の一種です このような増加が好ましくない理由は 3つあります
メモリスパイクでメモリが圧迫されると システムが対処します ダーティメモリのスワップおよび圧縮や 読み取り専用メモリの破棄 バックグラウンドタスクの終了を行います 最悪の場合 メモリスパイクによって アプリが停止することもあります メモリスパイクには 長期的な観点でも問題があります ヒープメモリ領域に 断片化やホールが発生するためです これを追跡する方法は2つです 特定のスパイクで 低点から高点までの間に作成され まだ存在している割り当てを見つけます または 広い範囲をまとめて選択し その範囲内で作成され破棄された 全割り当てに着目します Benが作成したトレースで試してみましょう
タイムラインで スパイクのある区間を1つ選びます トラックビューで スパイクの低い点から 最高点までクリックしてドラッグします すると 下の統計情報の詳細に 原因に関する情報が表示されます 行を総バイト数の順に並べ替えて 上位の原因を見てみましょう この例ではヒープメモリに注目していますが 上位のカテゴリには 仮想メモリのIOSurfaceなどがあります これは ここでの一時的なメモリの問題が 背景画像の 処理方法に関連することを示す 有力なヒントです 持続時間の順に並べ替えると スパイクの頂点に達した オブジェクトの中で目立つのは @autoreleasepool contentの ノードです 自動解放プールにしては かなりの数が作成されています これについては後ほど説明します 一時的なメモリの問題を調べる もう1つの方法は オブジェクトが作成され破棄される 原因となったコードを より広い範囲で探すことです ウインドウ下部のライフスパンのフィルタを に変更します タイムラインで 3つのスパイクすべてを選択します 次に 中央のJump Barで詳細ビューを に切り替えます は バックトレースによる 割り当ての詳細確認に適した方法です メモリが最も割り当てられている コードがわかります 合計を見ると 8GBの一時割り当てがあります これはどこから来るのでしょう 右のを見ると どこを調べるべきかわかります もう少し幅を広げましょう
コードのフレームが強調表示されており このリストを見ると makeThumbnail()のコードから 始めるのがよさそうです 1回クリックした場合が表示され ダブルクリックした場合ソースを確認できます
これが適用していた画像フィルタです ある行の項目で 数GBにおよぶ 膨大な量のメモリが作成され 破棄されていることがわかります これは一時的なメモリのはずですが スパイクの頂点に達するまで増加し その後 全部一度に解放されています 数フレーム上を見るために クリックして Jump Barのに戻ります 数フレーム上を見ると 今度は ThumbnailLoaderの loadThumbnailコードが気になります
ループでサムネールを フォールトしていて ループの実行中にメモリが増えており 最後に減少します 先ほどの自動解放プールのヒントと 合わせると 何が起きているのかわかります 自動参照カウント機能を持つ Swiftを使っていますが 自動解放プールはよく 一時的なメモリ増加の原因になります Objective-Cは このプールによって 関数の戻り値用に オブジェクトの寿命を延長します 自動解放プールは解放を遅らせることで これらの戻り値を存続させますが これは Swiftが Objective-C APIを使用または公開する フレームワークを呼び出す時に 自動解放 オブジェクトを生成できることも意味します この簡単な例では現在の日付を出力しますが 自動解放文字列も作成します この文字列は現在の自動解放スコープの 終了までヒープに維持されるため 一定の期間存続します
スレッドには通常 最上位の自動解放プールが あるものの 頻繁には整理されず プールがオブジェクトで一杯になる時 問題が生じます これはループ内で簡単に発生します
反復のたびに オブジェクトが同じプールに自動解放され 必要以上に存続する場合があります ループのすべてが終了するまで 存続するのです 内部的には 自動解放プールがコンテンツ ページを割り当ててオブジェクトを参照します これらは「Allocations」に表示されるため この種の問題の発生に容易に気づけます 後で自動解放プールが破棄される時に 遅延された解放をプールが送信し 多くのオブジェクトが一度に解放されます
これを修正するには 通常 ネストされたローカルの 自動解放プールのスコープを定義し 期間を絞り込みます この例では 自動解放されたオブジェクトは 内部のループごとのプールに保持され 反復のたびに解放されます つまり 蓄積されるオブジェクトの数が減り 参照の追跡に必要な コンテンツページが減ります 前に戻って 問題を解決できるか見てみましょう Instrumentsから ソースビューの右上のメニューを使って Xcodeでファイルを開きます
修正のために ループの本文に autoreleasepoolのスコープを追加します これにより 反復ごとにオブジェクトが破棄されます
さて うまく機能するでしょうか
おっと 開発用のスマートフォンを 忘れました Ben テスト用に あなたのを貸してもらえますか だめですよ私のですから Simulatorを使ったらどうですか なるほど そうします プロファイリングでは タイミングが正確になるよう 通常 実機でリリースビルドを実行します しかし ヒープ分析では Simulator環境が実際の動作と近いため メモリのプロファイリングに使えます メモリゲージに切り替え Benがお見せした機能を試してみましょう
Simulatorで ギャラリーを一度開いてから閉じます
ゲージは改善したようで メモリは 増えたものの大きなスパイクはありません
2回目も同様ですが 今回はあまり良くないパターンが 見えてきました
シートを3回表示した後 メモリスパイクが解消されたことが 確認できます 1ギガバイトに届きそうになっていません しかし メモリ使用量が毎回増加し 階段状になるという問題があります これはおかしいですね サムネール作成に大量のメモリが使われても ギャラリーを初めて開く時だけ 増えるはずです 自動解放プールの修正を すぐにプッシュします 楽しみを独占したくはありません Xcodeのデバッグバーから メモリグラフデバッガで一時停止します これにより アプリのヒープの すべての割り当てがキャプチャされます 増加の原因となった型がわかっている場合 すぐに探せます または 右側で共有できます このメモリグラフはInstrumentsで 直接インポートできますが AirDropでBenに渡しましょう 干草の山から針を見つけるようなこの難題に 彼ならきっといいアイディアがあるでしょう がんばれ Ben! Daniel 面白いけど そんな釣りにはもうひっかかりませんよ 自分のメモリグラフを使って このな増加について調べます 持続的なメモリとは 割り当て解除されないメモリのことです 持続的な増加は大抵このようになります 時間の経過とともにメモリが増えます この増加は複数の割り当てによるものです 「Allocations」instrumentの Mark Generation機能では 増加を期間に基づいて区分できます ボタンを押すと 割り当ての新しいグループが作成されます この世代は この時点より前に作成され トレースの最後まで存続している すべての割り当てを収集します これより後の時点を選択して 再度を押すと 新しいグループが作成されます この次世代は 前の世代の後から 新しいタイムスタンプまでに 作成された持続的な割り当てを すべて収集します Xcodeで独自のメモリグラフを生成し
Instrumentsにインポートしました 「Allocations」 「Leaks」 「VM Tracker」にデータが示されます 「Allocations」トラックに注目します トラックビューでは Danielの指摘と同じ 階段状のパターンが メモリレポートにあります Mark Generation機能を使って 増加のある期間に作成された 持続的な割り当てを分離します 増加のある期間のいくつかの時点を選択して ボタンを押します
Instrumentsに3つの世代が表示されました ギャラリーを開くことで生じた 持続的な増加が B世代とC世代でみられます いずれかの世代を展開して その割り当てを表示し 増加のサイズが大きい順に並べ替えて 最も増加の原因になった型を確認します この増加の大半は データストレージが原因のようです このタイプのエントリを展開すると 個々の割り当てと そのアドレスを確認できます 探し物が見つかりました
これらすべての データストレージの割り当ては ThumbnailLoaderコードで 作成されたようです では何がデータを保持しているのでしょう アドレスの1つをInstrumentsから取得し メモリグラフデバッガに入れると その参照元がわかります これにより ギャラリーを閉じた後も 存続している理由がわかります 拡張詳細ビューからアドレスをコピーし メモリグラフデバッガの フィルタバーに貼り付け 割り当てを選択します
メモリグラフデバッガの示す内容を より理解するために 仕組みを少し説明します
メモリ使用量の増加の調査では 割り当てが存続する理由や 何がそれを保持しているのかを 特定することが重要です これらを特定する上で メモリグラフデバッガが役立ちます これを最大限に活用するには 型情報と参照のスキャンについて 理解する必要があります 参照には主に4つのタイプがあります 1つ目は強い参照です 当然ながらこれはポインタで ARCで管理される場所にあり 所有権が明示的に保証されています 2つ目の弱い/非所有の参照も ポインタですが 所有権がないことが 明示的に保証されています 3つ目の非管理対象の参照は ランタイムが 認識している場所にあるポインタで 自動的には管理されません 手動で所有される参照が含まれますが それ以外の場合もあります 最後は 不確定または保守的な参照です スキャンするメモリの種類を ツールが 認識しておらず 生のメモリを確認する場合に記録されます 値がポインタに見える場合は そうかもしれませんが 型情報がない場合 それを確かめる方法はありません ツールはプロセスのヒープのスキャン時に 各割り当てに使用できる 最適な型情報を使用します このSwift Swallowの例では 最初の2つのフィールドは標準で 参照のスキャンにとって 重要なものは含みません その後 coconut参照をスキャンします このフィールドは ヒープ割り当てへのポインタを含むため Coconutオブジェクトへの強い参照です SwiftとObjective-Cの型情報は強力ですが CやC++には参照の所有権情報がないため 保守的な参照のみが使用されます ツールで実行できる最善の方法は 仮想メソッドによるC++の型名の検索です このクラスのインスタンスは Coconutとみなされます 仮想メソッドやその他の割り当てを 持たない型の場合 スタックトレースを使えば名前がわかります MallocStackLoggingデータでは このクラスのインスタンスに malloc in PalmTree::growCoconut() のようなラベルがつけられており これが何であるかについての ヒントになります 型情報と参照について説明できたので 前に戻って データストレージが持続的である 理由を確認しましょう メモリグラフデバッガを見ると 選択した割り当ては __DataStorageオブジェクトが保持し それをPhotoThumbnailが保持し さらに それを辞書が保持しています さらにさかのぼると これを保持しているのは 静的プロパティである ThumbnailLoader.globalImageCacheと わかります MallocStackLoggingを有効にしているため 右のインスペクタに 割り当てのバックトレースが表示されます インスペクタを使って この割り当ての ソースに移動します データを保持している PhotoThumbnailを選択します コードのクロージャの1つが その割り当てを実行しているようです スタックトレースを使って コードに移動します
見たところ このfaultThumbnailメソッドが サムネールをキャッシュし キャッシュミスの場合は 新しく作成しています このサムネールは先ほど見た globalImageCacheに 保存されているようです コメントによると URLとcreationDateに基づいて キャッシュしています これは妥当に思われますが バグがあります 明らかに ファイル作成時のタイムスタンプではなく 現在時刻になっています このため キャッシュ内に何も見つからず メソッドを呼び出すたび PhotoThumbnailを 新しくキャッシュしています サムネールの持続的な増加はこれが理由です ファイル作成日に基づいて キャッシュするよう修正しましょう タイムスタンプを間違えた コードを削除します 次は ファイル作成時の タイムスタンプを取得します Xcodeが必要なコードを提案してくれました Tabキーを押して決定します 再びアプリを実行して 問題が解決したことを確認しましょう 階段状のパターンがXcodeメモリレポートに 現れないことを確認します
機能を再度試してみます
サムネールを生成しました もう一度やってみます
増加はありません 念のためもう一度やってみます
メモリグラフデバッガ内で終了し その他の問題がないことを確認します
メモリリークがいくつかありました 横に黄色の三角形のアイコンがある 割り当てです Daniel またコードで リークを起こしたんですか はい次のトピックは リークしたメモリなので 丁度いいでしょう リークしたメモリを理解して修正するには まず到達可能性の説明が必要です プログラム内のすべてのメモリは 将来使用する予定の場所から 弱くない参照で到達可能である必要があります ヒープには3種類のメモリがあります 1つ目は有用なメモリです プログラムによって到達可能で 将来再び使用されます 2つ目は見捨てられたメモリです 到達可能で使用もできますが 実際に再び使われることはありません このメモリはアプリのフットプリントに 影響し 無駄です 過度なキャッシュや 高負荷データのシングルトンでの 保持などにより 簡単に発生します 3つ目のタイプのメモリは リークしたメモリです これは到達不能なメモリであり 二度と使用できません 多くの場合 手動で管理される割り当てや オブジェクトの循環参照により 最後のポインタが失われた時に生じます ほとんどのリークでは 1回の循環に 1つの参照を特定し修正することが目標です 例えば 誤った参照を削除したり 所有権の修飾子を 強から 弱または非所有に変更したりします これらのリークの調査をより簡単にするには フィルタバーの ボタンを使用します この場合も三角アイコンが表示されます
型がアプリのバイナリごとにグループ化され ナビゲータに表示されます コードがシステムバイナリから 型をリークする可能性もありますが 通常 リークの直接の原因は プロジェクト内の問題です フィルタバーの別のボタンをクリックし 自分のプロジェクトの型に絞り込みます 見やすくなりました リークは ThumbnailLoaderクラスに3つと ThumbnailRendererに3つあります そのうち1つを選択します これは ThumbnailRendererと ThumbnailLoader およびクロージャコンテキストの間の 小規模な循環参照のようです まずは このクロージャコンテキストの 用途を手短に確認しましょう Swiftクロージャは 値を取得する必要がある場合 キャプチャを保存するために ヒープにメモリを割り当てます メモリグラフデバッガは その割り当てに クロージャコンテキストのラベルをつけます ヒープ内の各クロージャコンテキストは 存続するクロージャと1対1で対応します デフォルトでは クロージャは強い参照を キャプチャするため 循環参照が生じ得ます 弱いまたは非所有のキャプチャを使えば 循環を解消できます 例えば 完了ハンドラを持つ Swallowオブジェクトがあり ツバメがココナツを運ぶと 呼び出されるとします 気をつけないと Swallow自身が強い参照として キャプチャされ 循環参照が生じます メモリグラフデバッガには 強いキャプチャとして参照が表示されますが クロージャのメタデータには 変数名は含まれません クロージャコンテキストからの参照はすべて 「capture」のラベルのみがつけられます 戻ってリークを解決できるか確認しましょう インスペクタを開いて いくつかの参照をクリックします
ThumbnailRendererには Loaderへの cacheProvider参照があります Loaderには クロージャコンテキストを 参照するcompletionHandlerがあります captureを選択してRendererに戻ると 強い参照であると インスペクタに示されています この循環を解消するには クロージャを 作成したコードを探す必要があります クロージャコンテキストの スタックトレースから PhotosViewのコードに移動します
このコードは ThumbnailLoaderオブジェクトを作成し 完了ハンドラを割り当てて ロードを開始するよう指示します しかし 先ほどの問題は クロージャがThumbnailRendererを 強い参照としてキャプチャし それが循環参照を起こしていることです どう修正すべきでしょうか コードを変更し 完了クロージャの代わりに Swiftの並列処理を使用できます しかし ここでは キャプチャリストを指定します 弱または非所有にすれば循環を解消できます Rendererのキャプチャを弱にして
オプションの弱い参照があるので このguard letを追加することで 参照先であるRendererが 使用時にまだ存在するようにします 今行ったのは3ノードの循環の修正ですが このような小さな変更が 大きな効果をもたらします この機能をもう一度試し メモリグラフデバッガで停止すると
リークした型は表示されなくなりました 型フィルタをオフにすると 驚いたことに その他のリークも解決しています 先ほど修正したリークは ほかの型も参照していたので それらの型の割り当ても解除されました この例のリークの特定と修正は 非常に簡単でした コードリークには様々な形があるため リークの特定は おそらく最も多くの疑問が生じる領域です いくつかお話しすると まずリークチェックで 見つからないものがあるのはなぜでしょうか リークを意図的に作った場合も 必ずしも ツールで検出できないのはなぜでしょう 対応する型情報がツールにないメモリは 多数ありますし C言語などでは 管理されていないポインタを使えます つまりツールは 一見ポインタのようで 実際はそうでない可能性があるものを 許容せざるを得ません ツールの保守的なスキャンでは ポインタを探して 参照と思われる値を1バイトずつチェックし 割り当てのリストと照合します 値が一致すれば ツールは ブロックへの不明確で 保守的な参照を記録します ただしそれらの値は 数値やフラグのほかに 有効なポインタのような ランダムなバイトの場合もあります そのため この疑問への回答としては 「保守的な参照では 実際のリークが 見逃される可能性があるため」です 意図的なリークを作成して それを見つける場合 これを100回実行するループに入れても 問題ありません 実際のアプリでのリークコードは通常 複数回実行されるため ツールはすべてのリークを検出するわけでは ありませんが その原因のバグを検出します これに関連する疑問が レポートされたリーク数が経時的に 変動するのはなぜか というものです リークはバグにより経時的に増えます ヒープはかなりノイズがありランダムです このノイズは保守的な参照を 非決定論的にするため 参照が現れたり消えたりします そのため プログラム起動時 オブジェクトが5つリークし ツールが最初5つリークを検出しても 後で4つになることもあります もう1つのよくある疑問は 返されない関数がメモリをリークすることが あるのはなぜかです 例えば noreturn属性を持つC言語の関数や Never型を返すSwift関数の場合です これらの関数は決して戻されないため 最適化されたコンパイラは 通常行う クリーンアップを省略できます これには コンパイラが作成するローカルの 割り当てや参照の解放が含まれます この種の関数は 非常に重要なアサートに 使用しても問題ありません プログラムがクラッシュするためです ただし スレッドを停止するのに 使われることもあります この例の Serverオブジェクトのような noreturn関数の呼び出しからの リークとしてローカルステートが レポートされた場合 明示的に グローバルに格納することで解決します オブジェクトをローカル関数の スコープ外に格納すると 参照はツールが認識できる場所に置かれます ツールが参照を認識できるため ローカル変数がコンパイラによって 保持されない場合でも オブジェクトはリークしているのではなく 到達可能であるとみなされます リークの説明は以上です 再びBenに交代し ランタイム速度と ココナツについて説明してもらいましょう ありがとう メモリを減らすと パフォーマンスを大幅に強化できます それをさらに強化できるように ランタイムの詳細を確認しましょう
weakとunownedは 強い循環参照の発生を防ぐために Swiftでよく使用されるツールです 2つの間の違いと いつ使用するかを説明します 弱い参照は常にoptional型ですが 宛先が初期化解除された後はnilになります 弱い参照の使用は ソースおよび宛先の 存続期間に関わらず常に許可されます ツバメとココナツの場合を考えてみましょう ココナツはツバメに運んでもらえますが ツバメを所有してはいません ココナッツにツバメを参照させる場合 強い参照の使用は避けるべきです 代わりに弱い参照を使用できます ただしオーバーヘッドが生じます Swiftでは 弱い参照を実装する場合 弱い参照の最初の実行時に 参照先オブジェクト用の Swiftの弱い参照ストレージを割り当てます この割り当ては Swallowと Swallowへのすべての弱い参照の間にあります これにより Swallowがいなくなった後に 弱い参照を 遅延的にnilにできます 弱い参照と異なり 非所有の参照は 参照先を直接保持します つまり 余分なメモリを使わず 弱い参照よりもアクセスにかかる時間が 短くなります optional以外の型にすることも 定数にすることもできます しかし 非所有の参照の使用は 常に有効とは限りません 例えば Coconutのholderの参照を 弱ではなく非所有にします 参照する前にSwallowがいなくなると どうなるでしょうか Swallowは初期化解除されますが 割り当て解除はされません そのため 非所有の参照は安全です 非所有の参照は何かを指している必要があり これによりランタイムは 「元オウム」や「ツバメ」を存続させておく ことができます(比喩を混ぜて話してます) この時点で Coconutの 非所有の参照を使用して Swallowにアクセスを試みると 決定的なクラッシュが発生します 非所有の参照はこの点で 強制的に アンラップする弱い参照に似ています 非所有の参照にアクセスしない場合も 放っておくのはよくありません 非所有の参照が存在する限り その参照先の割り当ては解除できず メモリを浪費します 参照先の存続期間がわからない場合 オーバーヘッドの小さい弱い参照は 有用です
メモリグラフに弱い参照や非所有の参照が レポートされない場合は Xcodeでプロジェクトのビルド設定の を 確認してみてください 可能な場合 デフォルトの レベルの使用を推奨します この設定にはツールに必要な 全メタデータが含まれ ツールにおける Swiftの精度を大幅に向上させます
具体例を見てみましょう このByteProducerクラスには generatorプロパティがあります これは defaultActionメソッドに 最初に割り当てられるクロージャです 問題は これによって 強い参照による循環が生じることです これは defaultActionメソッドが 暗黙的にselfを使用するためです メソッドをクロージャとして使う時は 注意が必要です
修正するには defaultAction()を 呼び出すクロージャを定義します selfのキャプチャは実行されますが 今度はキャプチャが明示的であり キャプチャリストを使用して 強い参照を回避できます 参照の修飾子を指定する必要があります ここでは weakが適切な デフォルトとして機能します この場合はunownedも使用できます これは ジェネレータクロージャが 参照先であるByteProducerインスタンスと 同じ存続期間を持つためです クロージャはほかのコードに渡されることも 非同期的にディスパッチされることも ないため キャプチャされたselfより 長く存続できません これらの選択肢によって パフォーマンスに 大きな差が出る場合があります このByteProducerを100万個割り当てて メモリグラフを エクスポートすると ヒープのコマンドラインツールにより コストの概要をすばやく把握できます ByteProducerごとに1つの 弱い参照ストレージが割り当てられます これらはByteProducer自体と 同じくらいメモリを消費します unownedではこのメモリは必要ありません 重要なのは weakが 参照の適切なデフォルト値であることと unownedでは 参照がその参照先よりも 長く存続しないことが保証できる場合に メモリと時間を節約できることです CPUオーバーヘッドの発生領域を 特定するには プロファイルを作成し swift_weakLoadStrong() などのランタイム関数の呼び出しを探します
Swiftの参照カウントの詳細に関する おすすめの資料は 「Automatic Reference Counting」の 「Swift Programming Language」の章です
弱い参照と非所有の参照のほかに 時々 自動の保持と解放の呼び出しが プロファイルのホットスポットになります ARCの回避は魅力的かもしれませんが 控えましょう 管理されていないポインタを使ったり パフォーマンスに 影響しやすいコードを メモリが安全でない 言語に移したりするよりも良い方法があります -whole-module-optimizationを 有効にしましょう インライン展開を増やして オーバーヘッドを削減できるためです 明示的な特殊化が必要なジェネリックを プロファイルを作成して探しましょう
また 最も頻繁にコピーする構造体には 単純なフィールドを含めるようにしましょう プロファイル作成は 高コストな 構造体のコピーの特定に有用です これらの構造体では 参照型やcopy-on-write型 および anyの使用を最小限に抑えてください
Swiftのパフォーマンスに関するヒントは 「Explore Swift Performance」と 「Consume noncopyable types in Swift」を 参照してください Objective-Cコードにも 保持と解放のオーバーヘッドを 削減する方法がいくつかあります
繰り返しますが ARCは回避しないでください 手動の参照カウントからのリークは デバッグが極めて困難になり得るためです メソッドをobjc_directとしてマークし Objective-Cメソッド呼び出しの インライン展開を可能にすると 保持と解放のトラフィックを軽減できます インライン展開ができない場合は パラメータの存続期間が保証されている 場合に コンパイラに知らせるために objc_externally_retained属性を使用すれば 保持と解放をなくせるため効果的です
パフォーマンスの一部は 監視のコストを反映します MallocStackLoggingとAllocationsは ライブデータを 追跡しますが これには すべての割り当てに関する 情報を記録するための メモリとCPUが必要です Leaks VM Tracker メモリグラフは スナップショットベースであり 分析中にターゲットアプリを 一時停止する必要があります そのため スナップショット処理中に アプリに一時遅延やハングが発生します 本セッションの内容をまとめます Instrumentsでのヒープ測定方法と 一時的および持続的な増加の パターンの探し方を説明しました 個々の割り当てに問題が見つかったら Xcodeのメモリグラフデバッガと MallocStackLoggingで 割り当てがヒープに存続している 理由を特定します とはいえ 何よりも予防が重要です アプリのヒープメモリを分析し 最適化しましょう リークと持続的な増加を見つけることで ユーザーはアプリをより長く楽しめます ご視聴ありがとうございました
-
-
10:01 - ThumbnailLoader.makeThumbnail(from:) implementation
func makeThumbnail(from photoURL: URL) -> PhotoThumbnail { validate(url: photoURL) var coreImage = CIImage(contentsOf: photoURL)! let sepiaTone = CIFilter.sepiaTone() sepiaTone.inputImage = coreImage sepiaTone.intensity = 0.4 coreImage = sepiaTone.outputImage! let squareSize = min(coreImage.extent.width, coreImage.extent.height) coreImage = coreImage.cropped(to: CGRect(x: 0, y: 0, width: squareSize, height: squareSize)) let targetSize = CGSize(width:64, height:64) let scalingFilter = CIFilter.lanczosScaleTransform() scalingFilter.inputImage = coreImage scalingFilter.scale = Float(targetSize.height / coreImage.extent.height) scalingFilter.aspectRatio = Float(Double(coreImage.extent.width) / Double(coreImage.extent.height)) coreImage = scalingFilter.outputImage! let imageData = context.generateImageData(of: coreImage) return PhotoThumbnail(size: targetSize, data: imageData, url: photoURL) }
-
10:23 - ThumbnailLoader.loadThumbnails(with:), with autorelease pool growth issues
func loadThumbnails(with renderer: ThumbnailRenderer) { for photoURL in urls { renderer.faultThumbnail(from: photoURL) } }
-
10:33 - Simple autorelease example
print("Now is \(Date.now)") // Produces autoreleased .description String
-
11:08 - Autorelease pool growth in loop
autoreleasepool { // ... for _ in 1...1000 { // Autoreleases into single pool, causing growth as loop runs print("Now is \(Date.now)") } // ... }
-
11:50 - Autorelease pool growth in loop, managed by nested pool
autoreleasepool { // ... for _ in 1...1000 { autoreleasepool { // Autoreleases into nested pool, preventing outer pool from bloating print("Now is \(Date.now)") } } // ... }
-
12:16 - ThumbnailLoader.loadThumbnails(with:), with nested autorelease pool growth issues fixed
func loadThumbnails(with renderer: ThumbnailRenderer) { for photoURL in urls { autoreleasepool { renderer.faultThumbnail(from: photoURL) } } }
-
17:27 - C++ class with virtual method
class Coconut { Swallow *swallow; virtual void virtualMethod() {} };
-
17:40 - C++ class without virtual method
class Coconut { Swallow *swallow; };
-
18:41 - ThumbnailRenderer.faultThumbnail(from:), caching thumbnails incorrectly
func faultThumbnail(from photoURL: URL) { // Cache the thumbnail based on url + creationDate let timestamp = UInt64(Date.now.timeIntervalSince1970) // Bad - caching with wrong timestamp let cacheKey = CacheKey(url: photoURL, timestamp: timestamp) let thumbnail = cacheProvider.thumbnail(for: cacheKey) { return makeThumbnail(from: photoURL) } images.append(thumbnail.image) }
-
19:28 - ThumbnailRenderer.faultThumbnail(from:), caching thumbnails correctly
func faultThumbnail(from photoURL: URL) { // Cache the thumbnail based on url + creationDate let timestamp = cacheKeyTimestamp(for: photoURL) // Fixed - caching with correct timestamp let cacheKey = CacheKey(url: photoURL, timestamp: timestamp) let thumbnail = cacheProvider.thumbnail(for: cacheKey) { return makeThumbnail(from: photoURL) } images.append(thumbnail.image) }
-
22:19 - Code creating reference cycle with closure context
let swallow = Swallow() swallow.completion = { print("\(swallow) finished carrying a coconut") }
-
23:11 - PhotosView image loading code, with leak
// ... let renderer = ThumbnailRenderer(style: .vibrant) let loader = ThumbnailLoader(bundle: .main, completionQueue: .main) loader.completionHandler = { self.thumbnails = renderer.images // implicit strong capture of renderer causes strong reference cycle } loader.beginLoading(with: renderer) // ...
-
23:40 - PhotosView image loading code, with leak fixed
// ... let renderer = ThumbnailRenderer(style: .vibrant) let loader = ThumbnailLoader(bundle: .main, completionQueue: .main) loader.completionHandler = { [weak renderer] in guard let renderer else { return } self.thumbnails = renderer.images } loader.beginLoading(with: renderer) // ...
-
24:24 - Intentional leak of manually-managed allocation
let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16) // intentional mistake: missing `oops.deallocate()`
-
25:12 - Loop over intentional leak of manually-managed allocations
for _ in 0..<100 { let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16) // intentional mistake: missing `oops.deallocate()` }
-
26:11 - Nonreturning function which can see leaks of allocations owned by local variables
func beginServer() { let singleton = Server(delegate: self) dispatchMain() // __attribute__((noreturn)) }
-
26:22 - Fix for reported leak in nonreturning function
static var singleton: Server? func beginServer() { Self.singleton = Server(delegate: self) dispatchMain() }
-
27:21 - Weak reference example
weak var holder: Swallow?
-
27:43 - Unowned reference example
unowned let holder: Swallow
-
29:07 - Implicit use of self by method causes reference cycle
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = defaultAction // Implicitly uses `self` } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
29:25 - Break reference cycle cause day implicit use of self by method, using weak
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = { [weak self] data in return self?.defaultAction(data) } } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
29:41 - Break reference cycle cause day implicit use of self by method, using unowned
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = { [unowned self] data in return self.defaultAction(data) } } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
31:14 - Struct with non-trivial init/copy/deinit
struct Nontrivial { var number: Int64 var simple: CGPoint? var complex: String // Copy-on-write, requires non-trivial struct init/copy/destroy }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。