この記事でのバージョン
Unity 2017.1.0f3
はじめに
今回は『Unite 2017 Tokyo』の講演の動画を見て、要点をまとめたり、追加で調べたり、
実際に手を動かしたりしながら、記事にもしちゃおうという感じの記事です。
そして、題材にする講演は、「最適化をする前に覚えておきたい技術」です!
講演者:黒河 優介(ユニティ・テクノロジーズ・ジャパン合同会社)
こんな人におすすめ
・最適化したいが、何をしてよいかわからず困っている人
・効率的に最適化を行いたい人
受講者が得られる知見
・Unity Profilerに関するノウハウ
・最適化の目算、見積もりをする技術
最適化とは
そもそも最適化とはなんぞやという話からですが、
端的に言えば、より良いゲーム体験のためプロジェクトを見直して適切な形に組み直す事です。
実作業としてはプロジェクトの設定、シーンの構成、スクリプトの内容等の見直し、修正をする事で
ゲームのクオリティを維持したまま、パフォーマンスの向上、メモリの削減を行う事です。
最適化はターゲットにしている端末で想定している処理速度が出ない時に行うもので、
ターゲットにしている端末で十分快適に遊べているなら無理して行う必要はありません。
最適化の手順
次に最適化の手順ですが、ざっくり以下のような感じです。
なお、今回のメインは1~3の話になります。
- 負荷がどれぐらいあるか把握
- 負荷となっている箇所を特定
- 負荷をどう減らすかの算段をたてる
- 実際に負荷を減らす
最適化で特に重要なのが、負荷となっている箇所を特定する事で、
これはパレートの法則に当てはめて考えると分かりやすいです。
全体の数値の大部分は、全体を構成するうちの一部の要素が生み出しているという理論。80:20の法則、ばらつきの法則とも呼ばれる。
ようはプロジェクトのごく一部を直せば、大体の問題は解決するので、
その一部分を見極めるのが肝心という事です。
そしてこの負荷となっている箇所を特定するのに必要なのがプロファイリングです。
プロファイリングとは処理ごとに掛かった時間、メモリの使用状況を確認する作業の事で、
プロファイラと呼ばれるツールを使って行います。
もちろん、Unityにも標準でプロファイラ(Profiler)が付いています。
なお、大まかな把握はUnityのProfilerで問題ありませんが、
詳細は知りたい場合は各ネイティヴのプロファイラを使いましょう。
Profilerの基本的な使い方
ProfilerはWindow -> Profilerから開く事ができます。
上部にCPUの負荷や使用メモリなど、各分類ごとのグラフが並び、
上部で選択している項目の詳細な情報が下部に表示されるようになっています。
実行中だと常に流れ続けてしまうので、確認したい場合はRecordを押して止めるといいでしょう。
より詳細な情報が欲しい時にはDeep Profileを有効にしましょう。
ただし、処理が重くなる点には注意が必要です。
また、Profilerはエディタ上だけでなく、実機上でも使えます。
プロファイリングの対象はConnected Player (バージョンによってはActive Profiler)から変更出来ます。
ただし、実機は同じネットワーク(Wifi)内か有線で繋ぐ必要があり、
Development Buildでビルドしたものでなければ、プロファイリングは行えません。
メモリの使用量が多い場合
最適化すると言っても状況によって対応手順は様々です。
なので、まずはメモリの使用量が多い場合の最適化についてです。
メモリの使用量が多い場合は以下のような手順で最適化を行なっていきます。
なお、「メモリが足りなくて落ちるとき」とありますが、
メモリの使用量が多いだけで落ちる事はなくても手順は同じです。
メモリリーク
まずはメモリが確保しっぱなしになっていて、
再利用出来なくなっている状態であるメモリリークが発生していないかを確認します。
メモリリークが発生しているとメモリ使用量がどんどん増えていくので、
メモリの使用量を観測すれば特定し易いです。
メモリ使用量の確認はProfilerの下部の詳細(Memoryの部分を押した状態)から行えます。
ここの数値が増加し続けて入ればメモリリークが発生している可能性が高いです。
また、長時間遊び続けてると落ちる場合もメモリリークの可能性大です。
なお、Detailedにした後、隣のTake Sample:Editorを押すとさらに詳細な情報を見る事ができます。
また、Memory Profilerというメモリ使用量を可視化するツールもあるんですが、
現状、Unity 2017.1.0f3では上手く動きませんでした。
メモリ使用量が多いのはC#?Unity?
メモリリークがないのに、メモリ使用量が多い場合は単純に使用してる量が多いという事になります。
この時に重要なのが、C#もしくはUnityのどちらで使っているメモリが多いのかという事になります。
これは判別するには、メモリリークの時同様、
Profilerの下部の詳細(Memoryの部分を押した状態)を確認します。
なお、UnityがUnityが使っているメモリで、MonoがC#が使っているメモリになります。
Unityのメモリ使用量が多い場合
Unityのメモリが多い場合は、TextureやMesh、Animationなどの
アセットのデータを大量に読み込んでいる事が原因です。
なので対処法としては余計なアセットが読み込まれないようにする、
同時に大量に読み込まないようにする(読み込みA→破棄A→読み込みBみたいな感じ)、
圧縮する(特にアセットストアから持ってきたものリッチ過ぎる事がある)などがあります。
C#のメモリ使用量が多い場合
C#のメモリが多い場合は、メモリを消費しないようにスクリプトを修正します。
特にファイルの読み込みや通信時にメモリを消費している場合が多いです。
なお、C#の場合は現在使っているメモリ、Used Totalだけでなく、
予約されているメモリ、Reserved Totalの方も重要になってきます。
C#の場合、一度使ったメモリはC#側で予約され、解放したとしても
ゲームを終了するまでUnity側で使う事が出来なくなります。(C#側での再利用は出来る)
なので、ファイルの読み込みや通信等で一度に大量に使ってる場合は注意が必要です。
また、エディタ上で動かしている時にProfilerを見るとエディタで使っている分も乗ってしまうので、
実機で確認する事をオススメします。
ロードが長い場合
ロードが長い場合は以下のような手順で最適化を行なっていきます。
プロファイリング中はDebug.LogをOFFに
便利なDebug.Logですが、その処理はかなり重かったりします。
なのでプロファイリング中は無効にしましょう。
場合によってはこれだけロード時間の短縮に繋がります。
無効にするには以下のようにDebug.logger.logEnabled を falseにします。
Debug.logger.logEnabled = false;
もしくはDebugクラスを書き換えるという方法もあります。
初期化が長い?ロードが長い?
ロードが長いと感じていても、実際はロード後の初期化処理が長い場合があります。
なので、どちらが長いが調べる必要があります。
どの処理に時間が掛かっているのかを調べるには、
ProfilerのCPU Usageで調べたいフレーム(今回は跳ね上がっている所)をクリックした後、
Profilerの下部の詳細(CPU Usageの部分を押した状態)を確認します。
なお、Time msをクリックすると処理に掛かった時間順にソートが出来ます。
上記の例ではNewBehavioursScriptのAwakeで71msも掛かっており、
ロードではなく初期化が長いのだと分かります。
なので、この場合はスクリプトを修正していく事になります。
なお、左上の項目をTimelineに変える事でMain Thread以外の処理も確認する事ができます。
ロードが長い場合
初期化ではなく、ロードが長いとなった場合、メモリリークの時のようにMemoryをDetailedで表示し、
読み込んでいるデータを確認します。
必要ないものがあれば読み込まないようにし、
データサイズが大き過ぎれば圧縮するようにすれば、ロード時間を短縮できます。
また、AssetBundleを使ってる場合、重複したデータを含んでいる場合があるので注意が必要です。
一瞬だけ画面が固まる場合
一瞬だけ画面が固まる場合は以下のような手順で最適化を行なっていきます。
Garbage Collection(GC)が発生していないかどうか
一瞬だけ画面が固まる場合はGCが発生している事が多いので、まずこれを確認します。
なお、GCとはメモリが足りなくった時に整理を行う事で、
未使用のメモリを再利用出来るようにする処理です。
メモリリークが起きなくなるため、便利な機能ですが、膨大な処理時間が掛かります。
GCを確認する際はProfilerのCPU UsageのGarbage Collector以外を
グラフに出さないようにすると確認しやすいです。(左の四角をクリックすると切り替えが出来る)
GCが発生していると以下のようにグラフがいきなり跳ね上がるので、容易に発見できると思います。
また、下部の詳細では実際にGCが起きている所のメソッドでGC.Collectと表示されます。
なお、ProfilerのGC Allocの項目で消費しているメモリを確認する事が出来ます。
(クリックでソートする事も出来ます。)
GCが発生している場合
GCの原因として多いのがstringの操作で、
文字列結合等をする度にメモリを使用してしまうので、StringBuilderを使うようにしましょう。
他にもRaycast系や、enumがKeyのDictionaryの参照等も(UnityやC#のバージョンによるかも)
メモリを消費してしまうので、Update等の頻繁に実行される所では使わないようにしましょう。
GCが発生していない場合
GCが発生していない場合、ロード等の単純に重い処理を実行してると思われるので
それを探し、修正していきます。
なお、その辺の話は後編の常にフレームレート低い場合をご参照ください。
おわりに
記事が長くなり過ぎそうなので、今回はここまで。
常にフレームレート低い時の最適化やProfilerの結果保存、
スクリプトの特定部分だけで処理負荷を測る方法については後編にて!
なお、実際の講演はより分かり易く、かつ、詳細な内容となっているので、
最適化?Profiler?という方はとりあえず一度視聴する事をオススメします!