メモリリークはかなり時間が経ってから現れる唯一のバグです。何かにメモリを割り当てた後、使用したメモリを解放せずにトラックできなくなると発生します。利用できる RAM を徐々に消費するにつれアプリケーションがダウンして、やがてクラッシュする原因になり、動作全体の安定性に影響をします。
すべてのリークの修正は大変ですが、一番大きいリーク元を見つけて、不要なメモリロスを確実に食い止める方法がいくつかあります。先日、Paragon チームは数時間後にゲームのクラッシュを引き起こす危険性があったメモリリークをこの方法で食い止めました。Paragon ゲームはシステム中の全メモリを使い果たすほど対戦が長丁場に発展することは滅多にありませんが、それでも展開の予測は難しいです。もしもチームが重要な対戦に参加した途端にクライアントがクラッシュしてしまったら、本当にいらいらすると思います。
今から紹介するたメモリリーク発生源をトラックするヒントとトリックは、そうした体験から編み出された方法です。
MemReport を使ってリークの兆候を見抜く
メモリリークをトラックする最初のステップは、リークが発生しているかどうかを判断することです。一番簡単な方法は、使用中のメモリ割り当てのスナップショットを時間差で 2 回キャプチャし、その変化を確認します。ゲームで何かアクションがない限り、時間が経過してもメモリ使用量はさほど変わらないはずです。ただし、時間が経過するにつれてメモリ使用量が増加していたら、リークを確認するサインです。
例えば、Tick ごとに 1000 整数が割り当てられるアクタを作成し、リファレンスをすぐに捨てます。アクタを空のシーンに置いて、10 分間動かします。そしてセッションの最初と最後の MemReport を見ます。2 つを比較すると、オブジェクト数は全く変わっていないのにメモリ使用量が明らかに増加しています。
MemReport の詳細は、過去のブログ 「メモリ最適化のデバッグ」 でご覧いただけます。
MemReport は、オブジェクトリークなどの他の種類のリークとメモリのリークを区別することができます。オブジェクトリークは、シーン内のオブジェクト数が増加し続けると発生することがあります。スポーンしたオブジェクトを正しく整理しないと起こりがちです。例えば、別のオブジェクトと衝突すると破壊される発射物クラスを作成したらどうなるでしょう。空気中に放たれた発射物は貴重なメモリで持ちこたえながら、空に向かって移動し続けます。 MemReport は、その時ワールドに存在する各オブジェクトのインスタンス数を表示することで、リークの発生源のオブジェクトの目星を付けることができ、スポーンおよびスポーン解除ロジックを調査することができます。
割り当ての解放
割り当てたメモリを解放し忘れた疑いがあれば、これらの割り当て元を突き止める必要があります。この作業を行うために、オープンなメモリ割り当てを監視するツールを Paragon チームが作成しました。このツールはまだ実験的でありデフォルトでは無効になっているので、MallocLeakDetection.h を変更して以下のように再コンパイルします。
#define MALLOC_LEAKDETECTION 1
再コンパイルを完了したら、Visual Studio が起動してゲーム インスタンスにアタッチされていることを確認してください。コンソールに “MallocLeak Start” とタイプするとロギングが開始し、“MallocLeak Stop” とタイプすると終了します。このツールにより、ロギングに解放と判断された割り当てはすべて Visual Studio の出力ウィンドウにダンプされます。または、ロギングの実行中に “MallocLeak Dump N” (N はバイト数) とタイプすることもできます。これにより、最低 N バイトの割り当てに対して解放された割り当てがすぐにダンプされます。ロギングはゲームを初期化した後に開始した方が良いです。割り当ての多くは初期化時に発生し、ゲームが終了するまで解放されません。
ゲーム開始後 10 秒で、リークの原因だったアクタを修正して割り当てを大きくしました。そして、割り当てている間に MallocLeak が起動していることを確認し、“MallocLeak Dump 1000000” を使って 1 メガバイト以上のすべてのオープンな割り当てをダンプしました。期待どおり、結果は怪しくなりました。かなりたくさんのメモリが、アクタの Tick 関数で割り当てられていました!
AllocSize:12345678, Num:1, FirstFrameEverAllocated:1522
UE4Editor-Core.dll!FWindowsPlatformStackWalk::CaptureStackBackTrace()
[d:\release-4.11\engine\source\runtime\core\private\windows\windowsplatformstackwalk.cpp:233]
UE4Editor-Core.dll!FMallocLeakDetection::Malloc()
[d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.cpp:180]
UE4Editor-Core.dll!FMallocLeakDetectionProxy::Malloc()
[d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.h:116]
UE4Editor-MemoryLeak.dll
UE4Editor-Engine.dll!AActor::TickActor()
[d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:807]
UE4Editor-Engine.dll!FActorTickFunction::ExecuteTick()
[d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:111]
UE4Editor-Engine.dll!FTickFunctionTask::DoTask()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:262]
UE4Editor-Engine.dll!TGraphTask<FTickFunctionTask>::ExecuteTask()
[d:\release-4.11\engine\source\runtime\core\public\async\taskgraphinterfaces.h:999]
UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:932]
UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksUntilQuit()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:679]
UE4Editor-Core.dll!FTaskGraphImplementation::WaitUntilTasksComplete()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:1776]
UE4Editor-Engine.dll!FTickTaskSequencer::ReleaseTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:530]
UE4Editor-Engine.dll!FTickTaskManager::RunTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:1435]
UE4Editor-Engine.dll!UWorld::RunTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:704]
UE4Editor-Engine.dll!UWorld::Tick()
[d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:1197]
UE4Editor-UnrealEd.dll!UEditorEngine::Tick()
[d:\release-4.11\engine\source\editor\unrealed\private\editorengine.cpp:1346]
UE4Editor-UnrealEd.dll!UUnrealEdEngine::Tick()
[d:\release-4.11\engine\source\editor\unrealed\private\unrealedengine.cpp:368]
UE4Editor.exe!FEngineLoop::Tick()
[d:\release-4.11\engine\source\runtime\launch\private\launchengineloop.cpp:2772]
UE4Editor.exe!GuardedMain()
[d:\release-4.11\engine\source\runtime\launch\private\launch.cpp:148]
UE4Editor.exe!WinMain()
[d:\release-4.11\engine\source\runtime\launch\private\windows\launchwindows.cpp:189]
UE4Editor.exe!__scrt_common_main_seh()
[f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:264]
kernel32.dll
ntdll.dll
多くのプロファイリング技術と同様に、結果を批判的に見ることが重要です。ロギング開始前に作成した割り当ては、結果に表示されません。ご注意ください。同様に、ロギング停止後に解放した割り当ては、リークの兆候でなくても表示されてしまいます。それでも MallocLeak ツールは、メモリの場所を簡単に調査し、問題のあるコードはどこなのかヒントをくれる素晴らしいツールです。リーク探しをお楽しみください!