パフォーマンス最適化、歩きは止まらないーーメモリ編2

[前回の記事](https://qiita.com/UWATechnology/items/30e6dbfc07ccb7ad9403)では、プロジェクト開発中のメモリ割り当て状況を紹介しました。その以外、開発チームが注意すべきより重要なところはまだ三つあります。それは、メモリリーク、Mono無効的なヒープメモリコスト、およびアセットの冗長性であります。これは、ほとんどすべてのチームが開発中に遭遇する問題です。 今日は、これらの問題の解決策について詳しく説明します。

---
#メモリリーク
メモリリークは、開発者がプロ​​ジェクト開発中に遭遇する最も頻繁や最も遭いたくない問題です。今から見れば、プロジェクトにメモリリークがあるかどうかを判断することについては、まだいくつかの誤解があります。

#####誤解１
プロジェクトのメモリ下がりは、シーンに出入りする前後に一貫ではありません。たとえば、シーンに入った後、メモリは40MB増加し、出た後は30MB減少しますが、システムに戻されないメモリが10MB残っているため、メモリリークが発生しました。

#####誤解2
プロジェクトがシーンに出入りする前後で、Unity Profilerのメモリ下がりは正常でしたが、AndroidのPSS値は完全に戻りませんでした（シーンを出た後のPSS値は、シーンに入る前のPSS値よりも高い）。つまり、メモリリークがあります。

上記は、開発チームが私たちにフィードバックする典型的な問題です。ほとんどの開発チームが同様の状況に遭遇すると思います。 **ここで、上記の2つの状況のどちらもメモリリークを示していないことを説明する必要があります。**メモリが一定期間に増加し続けても、メモリリークがあると単純に判断することはできません。メモリが完全に下がれないことを導く原因が多いです。**例えば、後で使用しやすいためにロードされたアセットがメモリに駐在すること、Monoヒープメモリは上昇するだけで下降しないことなど、これらの状況がメモリの完全下がりに妨害があります。**一般に、メモリがリークしているかどうかを判断するための推奨方法は次のとおりです。

####1、アセット、特にテクスチャ、メッシュなどの使用状況を確認します。
UWAがやったプロジェクトのディテールな最適化の過程で、リソースリークがメモリリークの主な形式です。具体的な原因は、ユーザーがロードされたアセットを保存する（たとえば、Containerに入れる）が、シーンを切り替える時にはRemoveやClearしませんでした。そのため、エンジン自体でも、Resources.UnloadUnusedAssetsなどの関するAPIを手動的にコールしてもアンロードできず、リソースリークが発生します。プロジェクト内のリソースの量が多すぎて、リークされたリソースを見つけるのが難しいですから、この状況を確認することは非常に困難です。**そのため、UWAレポートでは、プロジェクト内の各アセットの詳細なモニタリングを実施し、「ライブサイクル」指標を通じて、プロジェクト実行中における各アセットの使用範囲を明確に把握することができます。**
![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2a76fc28-14e6-fda0-03e4-e38b40728fde.png)
このようにして、アセットの「ライフサイクル」属性を使用して、メモリ内に「常駐」しているアセットをすばやく確認でき、そのアセットが「プリロードする」アセットであるか「リークする」アセットであるかを判断できます。

同時に、プロジェクトで使用されるアセットの総数は多い場合、数百または数千であり、すべてのアセットを人工的に1つずつ確認するのは非常に面倒です。**そこで、アセットの「シーン比較」機能をリリースしました。 次の2つの方法でアセットを比較して、「リーク」の問題があるアセットをより迅速に見つけることをお勧めします。**
![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/eee09e56-d5b4-8ddd-8c9d-076e9bb1648b.png)
#####●同じシーンまたは同じタイプのシーン間の比較
一般的に、ゲームプロジェクトのメインシティシーンやメインインターフェースシーンなどの同じシーンまたは同じタイプのシーンのアセット使用量はほぼ同じです。同じシーンのアセット情報を異なる時間で比較することにより、アセット使用量の違いをすばやく見つけることができます。このように、これらの「異なる」アセットの存在が妥当であるかどうかを判断するだけでよく、リークが発生しているかどうか、およびリークされていたアセットをすばやく究明できます。
![3-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/8267c751-8dc8-d642-65c5-059e76b02b69.png)
#####●異なるタイプのシーン間の比較
一部の常駐アセット以外、タイプが異なれば、アセットの使用量も完全に異なります。たとえば、一つゲーム内のメインシティシーンと戦闘シーンでは、部分の常駐アセット以外に両者が使っている大部分のアセットは全然違っています。したがって、2つの異なるタイプのシーンを比較することにより、比較結果で「共通アセット」を直接表示して、それが実際に事前設定された常駐アセットであるかどうかを判断できます。そうでない場合は、アセットが「漏洩」している可能性が高いため、プロジェクトのアセット管理に盲点がないかどうかをさらに確認する必要があります。
![4-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f65c84b8-950e-23c0-1e98-d07dd595cb84.png)
####2、ProfilerでWebStreamまたはSerializedFileの使用状況から検出します
AssetBundleの不適切な管理は、ある程度のメモリリークを引き起こす可能性もあります。つまり、前のシーンで使用されたAssetBundleは、シーンの切り替え中にアンインストールされず、次のシーンに移動します。この場合、Profiler MemoryにあるTake Sampleを使用して検出することをお勧めします。WebStreamまたはSerializedFileにあるAssetBundleの名前を直接に確認したら、「リーク」状況があるかどうかを判断できます。
![5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/febf9009-b616-8542-ddf5-dd8bb3c697fb.jpeg)
####3、Android PSS / iOSInstrumentからフィードバックしたアプリスレッドメモリで確認します
上記の「誤解2」を続けると、「Unity Profilerのメモリ下がりは正常でしたが、AndroidのPSS値は完全に戻りませんでした。」は可能です。この原因は、Unity Profilerがフィードバックするのはエンジンが実際に割り当てる物理メモリでありますが、PSSに記録されたものには、システムキャッシュの一部が含まれます。通常の状況では、AndroidまたはiOSはすべてのアプリのアンインストールデータを適時にクリーンアップしません。次回の使用をスムーズにするために、OSは一部のデータをキャッシュに入れます。自身のメモリが不足している場合、OS KernelはLowMemoryKillerみたいなルールを採用してキャッシュを照会したり、一部のプロセスを強制終了してメモリを解放したりします。だから、一回や二回の「PSS値は完全に戻らない」状況でメモリリークの問題を説明することはできません。
UWAが推奨する方法は、メインシティシーンと戦闘シーンなどの2つのシーンを切り替えることです。理論的に、同じシーンを複数回切り替えると、Profilerに表示されるUnityメモリが正常に戻れますなら、PSS / Instrumentメモリ値の変動範囲も安定する傾向があります。しかし、PSS / Instrumentメモリが増え続けますなら、注意を払う必要があります。この原因は、2つの可能性があります：

#####●Unityエンジン自体のメモリリーク問題。 
この確率は非常に小さく、以前はいくつかのバージョンでしか発生していませんでした。
#####●サードパーティのプラグインを使用しているときにメモリリークが発生しました。
この可能性は高いです。ProfilerはUnity自身のメモリしか監視できず、サードパーティライブラリのメモリ割り当てを検出できません。上記のメモリの問題が発生した場合は、最初に使用しているサードパーティのライブラリを確認することをお勧めします。

---
#無効なMonoヒープメモリコスト
現在、Unityで使用されているMonoバージョンには一つの大きな問題があります。メモリが割り当てられると、システムに戻されません。 これは別の問題につながりますーー**無効なMonoヒープメモリ**。Monoによって割り当てられたヒープメモリですが、実際には使用されていないため、「無効」と呼ばれます。**では、プロジェクトに「無効なヒープメモリ」が大量にあるかどうかを確認するにはどうすればよいですか？**

UWAレポートでは、次の図に示すように、プロジェクト実行中のメモリ割り当て状況を提供します。その中に、青い線と紫色の線の分離状況は、無効なヒープメモリの割り当てサイズを反映しています。たとえば、図で選択した時点で、青い線のReserved Totalは現在のプロジェクトが占有している物理メモリの合計でありますが、紫色の線のUsed Totalは現在のプロジェクトが使用している物理メモリの合計であります。つまり、現在のプロジェクトの空きメモリは57.1MBです。 （200.4-143.3）、これは主に2つの部分、空きUnityエンジンメモリと無効なMonoヒープメモリで構成されています。ぞの上、空きUnityエンジンメモリは17.1MB（92.0-74.9）であるため、現在選択されているフレームの無効なMonoヒープメモリは40.0MBです。さらに、図から、実行中に青い線と紫色の線は常に分離していることも分かりました。これは、「無効」状態になるMonoヒープメモリは常に存在していることを示します。メモリの「高価」モバイルデバイスにとって、これは非常に無駄なことです。
![6-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/27ae530b-5841-10ad-e18a-5678c0cf0784.png)
**では、どうやって過剰な「無効なヒープメモリ」の割り当てを回避または削減できますか？**UWAからの推奨方法は次のとおりです。
#####1.ヒープメモリの一次性の大き過ぎの割り当てを回避します。
Monoメモリの割り当ては「要求次第」で徐々に割り当てられます。ただし、大き過ぎの割り当てを一次性に開けると（例えば、一つの比較的大きいContainerをNewすることや一つの比較的大きい配置ファイルをロードすることなど）、必然的にMonoのヒープメモリが直接上昇することを導きます。そのため、開発チームは常にヒープメモリの割り当てに注意を払う必要があります。
#####2.必要のないヒープメモリのコストを回避します。
UWAレポートには、プロジェクト実行中のヒープメモリ割り当てTop10の関数をリストされています。文章の長さの限りで、ここでは繰り返しません。開発チームは[前回のメモリ最適化に関する記事](https://qiita.com/UWATechnology/items/30e6dbfc07ccb7ad9403)を見てください。

---
#アセット冗長性
メモリ管理の方に、注意すべき点はまだ一つありますーーアセット冗長性であります。私たちがテストした多くのプロジェクトでは、95％以上のプロジェクトがさまざまな程度のアセット冗長性を持っています。いわゆる「アセット冗長性」とは、特定の時間にメモリ内に同じアセットの2つ以上のコピーが存在することを指します。 この状況には、主に2つの原因があります。
####1．AssetBundleのパッケージ化メカニズムが導く
一つのアセットが複数のAssetBundleファイルに入力されます。例えば、一枚のテクスチャが異なるNPCによって使用され、各NPCが個別のAssetBundleファイルに作成されている場合、テクスチャの依存関係パッケージ化しないなら、テクスチャは異なるNPCAssetBundleファイルに表示されます。これらのAssetBundleがメモリに次々にロードされると、メモリにテクスチャアセットの冗長性があります。これについて、開発チームにアセットの冗長性問題を発見した後に関するAssetBundleの作成流れを検査することをお勧めします。

同時に、UWAレポートで各アセットに一つの判断標準を導入しましたーー「ピーク数」。これは、同じフレーム内の同じアセットの最大数を指します。1より大きい場合は、アセットに「冗長アセット」がある可能性が高いです。 この列で並べ替えると、プロジェクトのアセットの冗長性をすぐに確認できます。
![7-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/5ca07c22-cf34-986f-fd27-3cc33d36df82.png)
####2．アセットのインスタンス化が導く
Unityエンジンでは、特定のGameObjectのアセット属性を変更すると、エンジンはこのGameObjectのMaterialやMeshなどのアセットを自動的にインスタンス化します。マテリアルを例にすると、開発中にこのような仕様はよくあります：キャラクターが攻撃されたら、マテリアルの属性を変更して特定の攻撃を受ける効果を取得します。この仕様により、エンジンは特定のGameObjectbに一つのMaterialを再インスタンス化し、サフィックスにinstanceの文字を付きます。それ自体は特に大きな問題ではありませんが、Material属性を変更する要求のあるGameObjectが増えていると（例えばARPG、MMORPG、MOBA等ゲーム）、メモリの冗長性が大幅に増加します。

下図のように、ゲームが進行するにつれて、インスタンス化されたMaterialアセットは333に増加します。Materialは多くのメモリを占有しませんが、過剰な冗長リソースは、Resources.UnloadUnusedAssetsAPIのコールする効率にかなりの圧力をかけています。
![8-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e79ed1e2-8211-95b4-c5b6-811b5264ab23.png)
一般的に、アセット属性の変更状況はランダムではなく固定されています。例えば、GameObjectが攻撃されたときに、そのMaterialの属性変更は受ける攻撃タイプ次第で三つの異なるパラメータ設定があります。では、その要求に対しては、3つの異なるMaterialを直接作成し、Material属性を変更するのではなく、Runtime状況でコードを介して対応するGameObjectのマテリアルを直接置き換えることをお勧めします。こうすれば、何百何千個のinstance Materialがメモリ内で消え、代わりにこの三つのMaterialアセットがあることがわかります。この利点は、コチラまで読むあなたに、もう説明する必要はないでしょう。
##：）

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

UWA公式サイト：https://jp.uwa4d.com
UWA公式ブログ：https://blog.jp.uwa4d.com