메모리 누수(Memory leaks)는 증상이 나타나는 데 시간이 오래 걸려서 찾기가 어려운 종류의 버그입니다. 이런 버그는 메모리를 할당하고 나서 비워주는 것을 잊었을 때 생겨납니다. 메모리 누수는 가용 램을 전반적으로 불안정하게 만들고 프로그램이 버벅이게 만들다가 결국은 사용가능한 램이 바닥나 크래시가 납니다.
모든 누수 위치를 찾아서 복구하기가 어렵긴 하지만 사용하지 않는 메모리를 찾아내는 몇 가지 테크닉이 있습니다. 최근, 파라곤(Paragon)팀이 게임을 몇 시간 이상 하게 되면 크래시가 일어나는 증상을 해결하기 위해 이 테크닉을 사용하였습니다. 파라곤의 한 라운드는 모든 시스템 메모리를 소비할만큼 길지는 않습니다. 얼마나 진행 시간이 길어질지는 경기 양상에 달려 있어 예측하기 쉽지 않습니다. 그래도 영혼의 한타중에 크래시가 나면 정말 열이 뻗칠 테니까요!
아래는 메모리 누수를 추적하는 이들의 노하우입니다.
MemReport 에서 메모리 누수 단서 추적하기
메모리 누수 추적의 첫 번째 단계는 누수가 있는지를 알아내는 것입니다. 쉬운 한 가지 방법으로는 시간차를 두고 메모리 할당 스냅샷을 찍어서 비교하는 것입니다. 게임 속에서 아무런 행동도 취하지 않는 경우에는 시간이 흘렀어도 유사한 메모리 사용을 예상할 수 있습니다. 하지만 시간이 흐름에 따라 메모리가 일정하게 증가한다면 누수를 추적할 때가 된 것입니다.
예를 들어, Tick마다 천 개의 인티저를 할당하고 즉시 레퍼런스를 없애는 액터를 만듭니다. 액터를 텅 빈 씬에 올리고 10분간 게임을 실행시켜 줍니다. 그리고 MemReport를 시작할 때와 끝날 때에 실행합니다. 두 리포트를 비교하면 오브젝트의 갯수가 변경되지 않았음에도 메모리 사용이 꽤 증가하였음을 알 수 있습니다.
MemReport에 관련한 더 자세한 정보는 이전 블로그 포스팅(디버깅과 메모리 최적화)에서 보실 수 있습니다.
MemReport는 오브젝트 누수 등의 다른 종류의 누수들로부터 메모리 누수를 구분해 내도록 도와줍니다. 오브젝트 누수는 씬 속의 오브젝트 갯수가 지속적으로 증가하는 문제일 것입니다. MemReport는 특히나 적절히 제거되지 않은 오브젝트들을 집어내는데요. 예를 들어 다른 오브젝트와 충돌하였을 때 파괴(제거) 되는 발사체 클래스를 만들었을 때 이를 하늘로 발사한다면 어떨까요? 발사체가 중요한 메모리 공간을 차지하며 공중을 영영 떠돌 것입니다. MemReport는 각 시점에 월드에 존재하는 오브젝트의 인스턴스 갯수를 보여줌으로써 어떤 오브젝트가 누수를 일으키는지 알 수 있게 하여 스폰/제거 로직 중에 어느 부분에 문제가 있는지를 조사할 수 있는 단서를 줍니다.
개방된 할당 위치(Open allocations)
메모리 할당 위치가 비워지지 않았다고 의심이 들면 이 할당이 어느 부분에서 이루어졌는지를 추적해야 합니다. 파라곤 팀은 비워지지 않은 개방된 할당 위치를 감시하는 툴을 제작했습니다. 이 툴은 아직 실험단계 기능이고 기본적으로는 비활성화 되어 있습니다. 이 기능을 사용하시려면 MallocLeakDetection.h 를 수정하고 리컴파일을 하셔야 합니다.
#define MALLOC_LEAKDETECTION 1
수정을 하시고 나면, 비주얼 스튜디오가 동작하고 게임 인스턴스에 연결되어있는지 확인해 주시기 바랍니다. 콘솔에서 “MallocLeak Start”를 입력해서 로그를 시작할 수 있고, “MallocLeak Stop”를 입력해서 중지시킬 수 있습니다. 이 툴은 동작하는 동안 탐지된 개방된 할당 위치들을 비주얼 스튜디오의 output window에 출력해 줍니다. 선택 사항으로 툴이 실행중일 때 “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은 문제가 있는 코드 어느 부분을 들여다보아야 하는지 힌트를 주고 메모리가 어떻게 되고 있는지를 보여주는 좋은 툴입니다. 즐거운 버그 수정 되시길!