게임 개발이 특정 단계에 이르면, 정확히 무엇을 왜 메모리에 로드하는지 알아내는 것이 매우 중요해 집니다. 새로운 애셋들 제작에 따라 게임의 덩치는 커져만 가고, 로드 시간은 길어지면서 메모리 부족 문제에 시달리기 마련입니다. 다행히도 UE4 에는 메모리에 뭐가 왜 있는지 추척하기 위한 유용한 툴이 내장되어 있습니다. 저는 여기 에픽에서 이 툴을 사용하여 포트나이트의 메모리 사용량과 로드 시간을 최적화시키고 있습니다.
Memreport 로 문제 찾기
첫 단계는 항상 memreport 명령입니다. 사용하려면 ` 키로 콘솔을 열고 "memreport" 를 치면 간단한 보고를, "memreport -full" 를 치면 보다 상세한 보고를 받을 수 있습니다. 그런 다음 YourGame/Saved/Profiling/MemReports 폴더에 들어가서 맵 이름과 시간표 태그가 찍힌 .memreport 파일을 찾아봅니다. 이 파일은 간단한 텍스트 파일로, BaseEngine.ini 의 [MemReportCommands] (, -full 의 경우 [MemReportFullCommands]) 섹션 안에서 모든 명령을 실행한 출력 결과가 포함되어 있습니다. 원한다면 자신의 engine.ini 파일에서 게임에 실행하고자 하는 명령을 바꿀 수 있지만, 기본 명령으로도 간단히 시작해 보기에 좋습니다.
memreport 파일의 첫 부분은 전체적인 메모리 사용량과 함께, 플랫폼별 메모리 사용량과 모든 등록된 메모리 통계에 대한 섹션이 있습니다. "STAT_PixelShaderMemory" 같은 메모리 통계 이름에 대한 소스를 검색해 보면 정확히 어떤 것들이 기여하는지 확인할 수 있습니다. 다음 섹션은 "obj list -alphasort" 명령의 출력 결과로, 모든 UObject 클래스와 그 메모리 사용량을 나열해 줍니다. 그 이후에는 렌더링 메모리, 로드된 스트리밍 레벨 상태, 스폰된 액터에 대한 섹션이 따릅니다. -full 명령 역시 스태틱 메시나 텍스처같은 개별 애셋에 대한 섹션이 있어, 비싼 애셋을 찾아보는 데 유용합니다.
"obj list" 의 출력은 약간의 설명이 필요합니다. 포트나이트 리포트에서 몇 줄 살펴보겠습니다:
Class |
Count |
NumKBytes |
MaxKBytes |
ResKBytes |
ExclusiveResKBytes |
---|---|---|---|---|---|
AIPerceptionSystem |
1 |
0K |
0K |
0K |
0K |
AnimSequence |
19 |
1018K |
986K |
986K |
986K |
Material |
263 |
767K |
800K |
888892K |
1080K |
memreport 를 사용하면 메모리 상황이 어떤지 빠르게 확인할 수 있습니다. memreport 를 전후로 두 번 찍어 텍스트 diff 툴로 비교해 보는 것도 매우 좋습니다. 쉽게 diff 가능하도록 소팅되어 있습니다. 포트나이트의 예제를 통해 엔진 툴을 사용하여 특정 애셋이 로드된 이유를 알아내는 방법을 보여드리겠습니다. 먼저 "memreport -full" 출력을 검색하여 뭔가 잘못된 듯한 커다란 애셋을 찾는 것으로 문제를 확인합니다. memreport 의 이 줄 버전에서: 첫 열은 클래스 이름, 그 뒤에 해당 클래스의 인스턴스 수 입니다. 메모리 열의 경우, 처음과 마지막 열이 중요한데, 해당 클래스의 모든 인스턴스에 대해 누적된 메모리입니다. NumKBytes 는 메모리에서 UObject 의 바디에 사용되는 메모리 양인 반면, ExclusiveResKBytes 는 사운드 버퍼처럼 해당 UObject 에 전적으로 소유된 UObject 가 아닌 "리소스"에 사용되는 메모리 양입니다. UObject::GetResourceSize 는 오브젝트에 대한 리소스 크기를 결정하는 함수입니다. ResKBytes 는 공유 리소스가 포함되어있어 여기선 그다지 유용하지 않은데, 머티리얼의 경우 ResKBytes 총합은 머티리얼당 동일한 공유 텍스처가 한 번씩 포함되어 있어 인위적으로 늘어나 있습니다. 즉 특정 클래스에 사용되는 메모리 양을 확인하기 위해서는, 첫 번째와 네 번째 열의 값을 더해주면 됩니다.
StaticMesh .../S_Hex_Urban_Standard_03.S_Hex_Urban_Standard_03 1K 1K 14306K 3220K
메모리를 3MB 가득 차지하고 있는 스태틱 메시가 하나 있는데, 게임 해당 부분 플레이 당시에는 필요치 않아서 로드될 것으로 기대하지 않았던 메시입니다. 무언가가 이 애셋을 레퍼런싱하는 통에 필요치 않은데도 로드하고 있는 것입니다. 왜 그런지 알아봐야 겠습니다.
잘못된 레퍼런스를 추적하기에 좋은 기법은 두 가지 있습니다. 먼저 에디터를 로드한 다음 문제가 되는 애셋을 콘텐츠 브라우저에서 찾아봅니다. 그 후 거기에 우클릭하고 레퍼런스 뷰어를 선택하면, 다음과 같은 화면이 뜹니다:
레퍼런스 뷰어를 통해 레퍼런스를 탐색하며 오브젝트를 레퍼런싱하는 것이 무엇인지 빠르게 확인할 수 있습니다. 이 경우 블루프린트가 레퍼런싱하고 있는데, 그 블루프린트를 더블클릭해 보면 어디서 레퍼런싱하는지 확인할 수 있습니다. 이 방법으로 그 비싼 메시를 무엇이 로드하고 있는지 용의자를 몇 빠르게 잡을 수 있습니다.
다음에 사용할 툴은 obj refs 명령입니다. 프로파일을 찍었던 게임 인스턴스 안에서, 콘솔에 "obj refs name= S_Hex_Urban_Standard_03 shortest" 명령을 내립니다. 몇 초 후 GC 루트에서 타겟 오브젝트까지의 레퍼런스 체인에 대한 덤프가 로그에 출력됩니다.이 예제에 대한 샘플 출력은 이렇습니다:
(root) World /Game/Maps/Zones/Zone_Temperate_Urban.TheWorld->CurrentLevel
Level .../Zone_Temperate_Urban.TheWorld:PersistentLevel->ULevel::AddReferencedObjects()
FortWorldManager .../PersistentLevel.FortWorldManager_0->CurrentWorldRecord
FortWorldRecord .../PersistentLevel.FortWorldManager_0.FortWorldRecord_1->ZoneTheme
(standalone) FortZoneTheme .../ZoneTheme_Urban.ZoneTheme_Urban->HexTileClass
BlueprintGeneratedClass .../HexTile_Urban01.HexTile_Urban01_C->UClass::AddReferencedObjects()
HexTile_Urban01_C .../HexTile_Urban01.Default__HexTile_Urban01_C->Hex Deco Meshes
(target) StaticMesh .../S_Hex_Urban_Standard_03.S_Hex_Urban_Standard_0
이 출력은 절대 GC 되지 않은 루트 오브젝트에서 타겟 오브젝트까지의 레퍼런스 체인입니다. 공간 절약을 위해 경로를 단축시켰습니다. 출력 각 줄에 대해 (root) 같은 옵션 노트로 시작한 다음, 오브젝트의 클래스가 오고, 오브젝트의 전체 경로에 이어 마지막으로 해당 오브젝트의 어떤 측면에 레퍼런스가 저장되는지 나옵니다. 이 예제에서 일부 레퍼런스는 커스텀 AddReferencedObjects() 함수에 저장되어 있는 반면, 다른 것들은 편집가능 UObject 프로퍼티에 저장되어 있습니다.
그 출력을 위에서 아래로 읽어내려가다 보니, 블루프린트가 FortZoneTheme 에 레퍼런싱되어 있고, 차례로 그것은 포트나이트의 세이브 게임 시스템의 일부에 레퍼런싱되어 있음을 확인할 수 있습니다. 특히나 이 경우 FortZoneTheme 의 HexTileClass 프로퍼티를 TSubclassOf<> 에서 TAssetSubclassOf<> 로 바꾸려 합니다. 그러면 그 레퍼런싱된 블루프린트를 직접 로드하지 않는 한 로드되지 않겠지요.
이는 가능한 해법 중 한 가지일 뿐이지만, 제 경험상 불필요한 애셋을 로드하지 않도록 하는 것이 메모리를 절약하고 로드 시간을 단축시키는 데 가장 쉽고 효과적인 방법이었습니다. 메모리 최적화에 대해서라면 할 말은 많습니다. 질문 있으신 경우 영문 포럼 또는 네이버 카페에 해 주시기 바랍니다.