본문 바로가기
세미나

메모리 프로파일링, 최적화, GC

by gamedevlab 2025. 5. 14.

PC에서는 보통 16GB를 권장사항으로 잡음. (너무 작아보이지만 대부분의 텍스처는 VRAM에 들어가기 때문에 OK)

PS5는 12GB가 최대 메모리 (시스템메모리+GPU메모리 공용) - 백그라운드 시스템 메모리를 위해 1GB 뺀 11GB 이하가 요구조건.

일반적으로 시스템메모리에 cpu연산을 위해 데이터 로드 → 현재 씬에 필요한 데이터를 VRAM으로 넘김 (보통 VRAM에서 시스템메모리로 다시 던지는 경우는 없음. 느리고 불필요하기 때문.). 중복이 발생하지만 상관없음. VRAM은 내부api를 통해 더이상 캐시할 필요가 없어지면 메모리에서 자체적으로 내림. → VRAM 이상시에는 애프터버너같은 툴을 통해 씬마다 VRAM도 측정

 

에디터와 패키징

공통

로우포맷 파일(png,bmp,fbx,wav etc) → 에디터에서 임포트 uasset으로 변환(1차 압축 및 변환. 파일크기 축소. 메타데이터만 가지는 경우도 있음.) → 패키징 (upak을 사용하면 uasset들을 전부 압축. upak을 사용하지 않으면 uasset 상태로 패키징)

(플레이시 pak을 마운트 → pak을 읽기 위한 해시 영역을 메모리에 올림 → 요구받은 해시를 따라 pak에서 읽어옴.)

텍스처의 경우 oodle 압축 포맷을 사용

ASTC 압축 옵션 → 각각의 옵션에 따라 속도 차이를 분석한 페이지가 있는데, 플스 SDK8부터는 ASTC 버린다고 하니까 볼 필요가 없어졌다(지금은 7.x이고 곧 올라갈것)

ASTC가 웬만한 콘솔,모바일기기에서 퍼포먼스상으로 가장 권장되는 옵션이였는데 왜 버려졌는가? 아마 크라켄 때문으로 추측함.

 

기본 메서드는 크라켄. OODLE Texture Rate Distortion Optimization( RDO )는 40정도 사용. 3메가짜리를 RDO 0에서 650KB, RDO 40에서는 450KB정도로 압축.

PS5에서는 BCn / ASTC → 우들 크라켄 압축 세팅을 사용했을때를 기준으로 함 (크라켄이 Zlib과 유사하나 조금 더 좋다는 글들이 많은데, 실제로는 압축 해제 속도는 3~5배까지 빠르고, 거의 50%까지 텍스처 압축이 가능함. PS5 크라켄 HW 다이와 함께 작동시킬때 가장 좋음)
https://gamingbolt.com/former-frostbite-software-engineer-explains-ps5s-kraken-compression-unit

PS5 기기특성상 크라켄 압축해제를 담당하는 하드웨어 단말이 있음. (cpu,gpu가 하지 않는 전용 하드웨어가 있음)

무조건 크라켄을 쓴다.

그러나 메모리에 올릴때는 압축 해제. 즉, 패키지 용량에만 관여함.

메모리에서 크기를 줄이려면 기본 압축 포맷 변경이 필요 (BC1 등.)

애니메이션도 자체 압축 코덱이 있고, 다른 에셋들도 마찬가지.

https://www.radgametools.com/oodletextureexamples.htm

 

Oodle Texture RDO Examples

Oodle Texture RDO Examples Oodle Texture works on all kinds of images and textures. It has been tested extensively on real game source art, in all BC1-BC7 formats, on albedo, normal maps, RGBA textures with four scalar material channels packed together, al

www.radgametools.com

우들 공홈의 크라켄 압축에서 직접 크기 감소율을 변경하면서 확인해보면 40%를 떨궈도 시각적 손실이 거의 없다.

PS5 타겟 패키징 옵션에서도 크라켄이 기본으로 잡혀있고, 압축 파라미터도 확인 가능

 

+ 옆에 있는 옵션들도 많다. HFR도 설정 가능하고, 컴프레션 퀄리티로 한번에 낮출 수도 있음. 하드웨어 가속된 콘볼루션 옵션을 켜면 DSP(digital signal processing) 칩으로 더 싸게 연산을 한다는데, DSP칩은 사운드 관련 하드웨어라 나중에 사운드쪽을 튜닝할 때가 되면 살펴볼것.

압축률 RDO를 2:1정도 쓰는게 평균. 텍스쳐그룹으로 묶어서 관리하라고 주석에서 설명한다.

https://qiita.com/EGJ-Nori_Shinoyama/items/8b3c2e4e372f05e33476

 

[UE4] Texture GroupがTexture Streamingに与える影響 - Qiita

Texture Groupってなに?UE4では、各種TextureにTexture Groupという設定項目があります。この設定は、そのGroupに属するテクスチャらの最大解像度であったり、Mi…

qiita.com

텍스쳐그룹의 로딩 우선순위 설정

 

upak. (20기가 남짓으로 압축). upak은 분할압축도 가능. upak1,upak2… 각각은 청크라고 부른다.

 


몇가지 방법을 소개. 큰 방법에서 세부 방법으로 나열

언리얼 인사이트 - 메모리 인사이트 (플로우를 관찰하기에 적합. 마지막에는 결국 이걸로 관찰)

memreport , obj list , obj refs (memreport는 다양한 obj 커맨드를 내장하고 있음. memreport에서 1차 분석 후 obj list 커맨드로 세부 분석. 왜 메모리에 올라갔는지 보려면 obj refs)

에디터에서는 레퍼런스뷰어, 통계 툴로 참조 관계 파악 (obj refs, obj list와 거의 동일한 기능) 수가 많으면 인스턴싱, 참조관계 제거 등.

에셋에서는 sizeMap(레퍼런스 뷰어와 같으나 내부에 링크된 리소스 사이즈를 보여줌), 불필요한 참조 제거, 에셋 타입별 에디터에서 불필요한 요소를 삭제, 에셋 크기 변경, 압축 포맷 변경, 눈에 띄지 않으면 대체 등.

 

프로파일링 과정은 수사 과정과 유사. 재미있다.

브리핑에서(인사이트) 수상한 포인트 발견 → 해당 포인트의 현장 검증(메모리 리포트) → 눈에 띄지 않으면 가설을 세워 용의자를 추려냄 (필터링) → 심문 및 테스트(가설 검증) → 해결될때 까지 가설->용의자->심문 프로세스 반복.

메모리의 경우는 딱히 가설이 필요 없기 때문에 가방정리에 가까움. 나도 모르게 끈으로 묶여서 가방에 딸려들어간것들(참조)을 잘 봐야함


언리얼 인사이트

패키지된 게임 바로가기 생성, 바로가기 속성 - 대상 위치 맨 뒤에 exe 에서 한칸 띄우고 -trace=default,stats,file,loadtime,assetloadtime,task,counter,Contextswitch,StatNamedEvent 등 trace 키워드를 넣을 수 있음.

메모리 분석에 필요한건 -trace=default,memory 두가지. (나머지 키워드들은 cpu , gpu 분석 시에 그대로 사용한다.)

(default=cpu,gpu,frame,log,bookmark,screenshot,region) 포함됨

실행 전에 언리얼 인사이트 실행해두면 바로 분석 가능

<엔진경로>\Engine\Binaries\Win64\UnrealInsights.exe

만약 파일이 없다면 언리얼 인사이트 분석 툴을 한번도 연 적이 없어서 그럼. 에디터 우측 하단 트레이스-언리얼인사이트(세션브라우저) 를 클릭하면 몇분의 컴파일 이후에 해당 파일이 생성됨.

자주 사용하게 되므로 UnrealInsights.exe 바로가기를 따로 만들어두던가 작업표시줄에 등록해두고 쓰는게 좋다.

이걸 실행하지 않고 게임 바로가기를 그냥 실행하면 saved/profiling/ 디렉토리에 저장되는데, 언리얼 인사이트 공용 폴더에 저장되지 않으므로 찾아서 보기가 불편해진다.

인사이트 -커넥션- 채널에 직접 입력 후 실행된 게임에 커넥트하는것도 가능함.

 

이 때 에디터는 꺼두어야 한다. 인사이트가 에디터랑 연결되면 제대로 기록이 되지 않기 때문. 패키징 중에 분석도 불가능함.

인사이트 실행 - 게임 바로가기 (-trace를 붙인것) 실행 시에 인사이트에 새로 기록되는것을 볼 수 있음. 라이브로 보는것도 가능한데, 불편하고 느리기 때문에 한번 플레이를 쭉 한 뒤에 따로 열어보는것이 좋음.

트레이스를 멈추려면 게임을 종료하거나 ` + trace.stop 을 쓰면 됨.

+ 트레이스 태그 중에 bookmark를 넣어서 실행한다면, 게임 도중 내가 마킹하고 싶을 때 `+trace.bookmark 무슨무슨상황 이라고 입력하면, 트레이스 파일의 그래프에 북마크가 표시되므로 분석이 용이함.

(트레이스 도중에는 god, slomo, pause 등의 치트, 기능들을 적극 사용하는것을 권장함.)

시나리오 대로 태스크를 플레이한 뒤에 트레이스 파일을 열고 메모리 탭으로 들어가면 전체적인 그래프가 나온다.

 
 

피크를 찍는 부분들이 존재할텐데, 이곳을 깎아내야 한다. (현재는 두드러지지 않고 전반적으로 높음. 이러면 이상한것. 사용하지 않는것들을 미리 들고있다는 말.)

전투 상황이라면 런타임 가프쪽 리소스, 전투 관련 이펙트 등이 주로 사용될 것임. 이 중에서 용량으로 소팅 후 불필요하게 고품질인 리소스 크기를 줄이면 됨.

개활지 상황이라면 뷰 디스턴스를 조금 줄여보거나, PCG, 건물 디테일, VFX 등 월드쪽의 리소스를 위주로 살펴보면 됨.

https://dev.epicgames.com/documentation/fr-fr/unreal-engine/memory-insights-in-unreal-engine 세부 내용 참고.

우측의 쿼리 버튼으로 A-B 구간의 사용 메모리 분석 (개발자용. 현재는 존재만 알면 됨.)

 
 

그래프화가 가능한가?

각각의 상황마다 팀별 파이를 분석, 대략적인 가용 메모리를 분배하면 좋겠다

→ 기본적으로는 불가능. 폴더별로 분류되지 않고 오브젝트별로, 타입별로만 분류되기 때문. 따라서 시각적 분포 확인까지 만들려면 생각보다 까다로움

ex) textureList ~~ 가 있으면, 어떤게 VFX거고 어떤게 캐릭터건지..

에셋 경로로 따진다면 파서와 규칙이 필요함. 레퍼런스 관계가 복잡할수록 특정 팀의 잘못이라고 하기 어려워짐. 내부에 포함하는 관계의 메모리를 알려주지 않음.

+ 본질적으로 UObject가 아닌것들은 엔진의 memreport로 표시되지 않음. 프로세스 메모리, 엔진을 돌리기 위한 메모리, 외부 라이브러리에 필요한 메모리(Chaos) 등은 표시되지 않음.


메모리 리포트, obj 커맨드

피크를 찍는 부분을 인지한 뒤에, 다시 게임을 진행 (이번에는 트레이스 실행할 필요 없음) 해당 시점이 되면 ` + memreport -full 을 입력.

saved/profiling/memreport에 메모리 분석 파일이 생성됨.

해당 파일을 텍스트 뷰어로 열어서 현재 시점의 메모리 사용량 분포를 알 수 있음.

Memreport 내용과 Memreport -full의 내용

  1. 전체 플랫폼 메모리 통계
  2. 텍스처 메모리 및 메모리 그룹 통계(바이트)
  3. 로드된 모든 객체 목록, 장면의 인스턴스 수, KB 단위 크기
  4. RHI 자원 메모리 ← 볼 필요 없음
  5. 현재 로드된 레벨
  6. 지속적으로 생성된 Actors
  7. 입자 통계
  8. 구성 캐시
  9. 풀링된 렌더 타겟
  10. 모든 텍스처
  11. 입자 시스템
  12. 사운드 객체
  13. 스켈레탈 메시 객체
  14. 정적 메시 객체
  15. 레벨
  16. 정적 메시 구성 요소
  17. 총 개체 수

읽는법.

Obj List - 클래스,카운트,NumKB,ResExcKB(총바이트),ResExcDedSysKB(전용시스템메모리할당), ResExcShrSysKB (공유시스템메모리할당),ResExcDedVidKB (전용비디오메모리),ResExcShrVidKB (공유비디오메모리),ResExcUnkKB (알수없는메모리할당)


Class=[ClassName] 등의 인자로 필터링은 가능하지만 전체 분석에는 부적합
Obj list class=texture 같은 형식.

여러가지 아규먼트를 넣을 수 있음. -CSV(csv로 저장) -resourceSizeSort -alphasort 등.

obj refs name=경로/SKM_Gavi_Hair_ACC 처럼 오브젝트를 참조하는 객체만 뽑아내는것도 가능.

이것은 GC 루트, 클러스터링을 분석할 때도 필요함.

맨 아래 섹션에서 다루겠음

 

+ list2 커맨드를 쓰면 오브젝트의 하위 클래스까지 보여준다.

+ RedirectToFile Profiling/CSV/objlist.csv obj list -csv -all 같이 RedirectToFile <경로/이름> <커맨드> 를 쓰면 해당 경로에 해당 커맨드 출력을 뽑아낼 수 있다.

 

 

기본적으로는 로그파일 영역을 복사 - 엑셀로 붙여넣기 후 사이즈 내림차순으로 보면 편하다.
(세부분석을 위해서는 obj refs name=ThirdPersonCharacter_C (객체가 참조되고있는 오브젝트 리스트) 처럼 다른 obj 커맨드를 사용해야 함.)

https://qiita.com/donbutsu17/items/dd9e00bee27d6868ed3d 다양한 obj 커맨드 목록.

참조가 없다는 가정 하에 소팅 후 문자열 비교 -> 뭐가 새로 로드됐는지 알 수 있다. (이상적인 상황)

+ 엔진 기본 기능에 Obj List forget 과 remmber 가 있다. forget은 현재 obj list의 결과물을 “잊는다.” 즉, 다음번의 obj lsit는 현재 찍은것을 제외한 새로 로드된것만 출력된다. remember는 잊은것을 떠올린다.(reset) 이걸 써도 상관없다.

레벨 오픈시 레퍼런스 체인으로 잔뜩 로드됨.
소프트 레퍼런스만 있는 경우라면 사용하기 전까지 로드되지 않음(파일경로만 있는 경우 등). 그러나 하드 레퍼런스가 있는 경우 된 경우 무조건 로드됨.(UPROPERTY 클래스포인터 변수 등, 생성자에서 로딩한 경우 등)
우리는 비동기로딩, LazyLoad하는 방식을 잘 사용하지 않음(경로만 들고 FindObject 등을 쓰는것). 대부분 UPROPERTY를 이용하는게 관계가 한눈에 들어오기 때문.

비교할 포인트 찾기. -> A지점에서 obj gc 커맨드 입력 후 memreport , B지점에서 다시 obj gc커맨드 입력 후 memreport
(obj gc = 즉시 gc 실행 후 gc타이머 리셋. 유사한 함수로 gc.CollectGarbageEveryFrame 1이 있음.)

경험적으로 메모리의 대부분은 텍스처.

 

엑셀에 붙여넣기 후 데이터->텍스트나누기->구분 기호로 분리됨 → 공백 선택 ->정렬 완료

상단의 Class~ResExcUnkKB까지 선택 후 데이터-필터 선택. 이제 화살표 클릭 후 오름차순-내림차순 선택 가능

 

ResExcKB 소팅 후 10MB 이상인것만 선택.
 
 

SkelMesh>BodySetup>Texture2D>StaticMesh 까지만 예시로 해보자.

스켈메시-불필요한 로드 제거시 300MB 이상 절약 가능함

당장 로드될 필요가 없는 에셋은 로드하지 않는다.
 


레퍼런스 뷰어로 확인 or obj refs로 확인

A는 DT에 약한참조로 저장되어있으나, 애님BP가 A와 레퍼런스관계인데, 강한참조로 들어있음.

 

강한참조인 애님BP를 삭제하면 레퍼런스 관계가 끊긴다.

게임에서 ` + obj refs Name =/Game/<프로젝트명>/파일경로 를 쳐보면 로그가 쭉 찍힌다.

 

 

UObject로 캐스팅을 하면 하드레퍼런스가 걸린다. (소프트 레퍼런스 내에서도 타입은 하드, 값은 소프트 → 타입은 액터 or 네이티브 클래스, 값은 기본값=테이블 처럼 세팅하는게 진짜 소프트 레퍼런스.)

아래는 소프트 레퍼런스로 변경하는 방법과 SizeMap 쓰는 방법(로컬 파일 사이즈 분석). (계속 써야하는것들이라면 들고있는게 좋다. ex)총알.)

https://udn.unrealengine.com/s/question/0D5QP000006oe6s0AA/changing-a-hard-reference-to-a-soft-reference-throughout UDN에도 하드레퍼런스를 소프트레퍼런스로 변환하려는 니즈가 있음.

 

이런 경우에는 구조 변경이 불가피하다.

  1. 데이터테이블의 하드레퍼런스를 소프트레퍼런스로 변경하기 (수정이 많이 필요하지만 DT 하나만 수정해두면 계속 사용 가능. 단, 규칙성을 위해 존재하는 모든 DT들도 비슷한 작업을 해야됨)
  2. 레벨별 코스튬 DT를 따로 만들고 거기에 레퍼런싱. (레벨이 9개가 되면 테이블 하나 수정하는 대신 9개를 수정해야함.)
  3. 런타임 레벨 초기화시 레벨에서 안쓰는것들은 동적으로 지워버리기. (가능하다면 베스트.)
    → 이 방법을 쓴다면 프록시DT를 만들고 DT매니저에서 레벨 호출시 화이트리스트를 긁어와 화이트리스트인 행만 읽어서 새 DT를 만들어서 전달. 레벨에서는 이걸 계속 쓴다.

스켈메시의 경우 이런식으로 불필요한걸 제거하거나 비싼걸 싸게 만든다. 반드시 스켈메시일 필요가 없다면 스태틱메시로 대체하고, 티가 나지 않으면 같은걸 돌려쓴다.

스태틱메시도 마찬가지. 불필요하면 제거, 싸게 만들기, 티가 안나면 같은걸 돌려쓰기. ISM으로 묶어버리기.

바디셋업 - 모두 적절하게 설정한다면 100MB이상 절약 가능

각 인스턴스의 물리 처리를 위해 사용되는 클래스.

메모리리포트에 바로 표시되지 않으므로 게임에서 ` + obj list class=bodysetup -resourcesizesort 입력 후 로그를 열어본다.

 

보면 각각의 사이즈는 작다.

총합 사이즈가 큰것은 그만큼 오브젝트가 많기 때문.

복잡도를 줄이거나 수를 줄인다.

첫번째 F_Shrub_005_a을 열어보면 심플콜리전은 없는데, 복합콜리전은 있다.

 

복합콜리전 절대 안쓸거면 복합콜리전을 삭제해야한다.

 

 

 

텍스처

사이즈 줄이기

VT로 변환하기

압축 안된것들 반드시 압축하기 + 불필요한 포맷의 경우 압축률이 높은 포맷으로 변경.

+ 동일한 텍스처를 복제해서 사용하는 경우 제거 (파이썬 스크립트 돌려야됨. https://forums.unrealengine.com/t/better-way-to-check-for-duplicate-textures/270396 )

 
 
 

스태틱메시

대부분 바디셋업과 동일한 문제

 
 

에디터 통계 툴에서 피직스 옵션 변경 전후 비교(8메가->2메가)

+ 에디터에서 확인하려면 툴->검사->통계 툴을 확인할것. 패키징 된 결과물과 사이즈는 다르지만, 대략적인 분포, 뭐가 큰지를 한눈에 볼 수 있다.

+ 원거리에서 보는 용도인데 버텍스 수가 너무 많다. 이런 유형들은 알아서 줄일 필요가 있음. 트라이앵글로 정렬해서 수정.

나이아가라의 경우 ListParticleSystems -resourcesizesort 커맨드로 정렬 가능

CompTrueResSize만 보면 된다.

 

레퍼런스

오공

로비 6GB

시네마틱 구간 8~9GB

플레이구간 8~11GB

 
 
 
 

로딩 범위

 

라오어1 시네마틱 9~10GB

플레이구간 10~13GB

 
 
 

심화.

타겟 게임은 레벨 로드시 피크. 11GB이상 찍음. 시간 지나면 10기가 언저리.

GC클러스터 옵션을 끄고 패키징하면 6~9기가로 줄어듬.

GC가 각 오브젝트별로 내릴지 검사하는 대신에 연관성있는 오브젝트끼리 클러스터링해서 클러스터에 쓰이는게 하나라도 있으면 클러스터 전체를 유지하는 방식.

공식 문서에는 대부분 이걸 켜놓는게 유리하지만 끄고 쓰는것도 한번 테스트해보라는 식으로만 써있음.

시간복잡도는 줄지만 공간복잡도가 올라감.

타겟 프로젝트는 아직 이걸 끈 상태로 플레이해본적이 없으므로 사이드이펙트 가능성 높음 (60초마다 히치가 걸린다던가…)

하지만 끈다면 메모리 2기가 이상 확보 무조건 가능함.

+ 가비지컬렉션 시스템

  • 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템
  • 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적
  • 마크-스윕(Mark-Sweep) 방식의 가비지 컬렉션
    1. 저장소에서 최초 검색을 시작하는 루트 오브젝트 표기한다.
    2. 루트 오브젝트가 참조하는 객체를 찾아 마크(Mark)한다.
    3. 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 계속 반복한다.
    4. 이제 저장소에는 마크된 객체와 마크되지 않은 객체의 두 그룹으로 나뉜다.
    5. 가비지 컬렉터가 저장소에서 마크되지 않은 객체(가비지)들메모리를 회수한다. (Sweep)

언리얼에서는 해당 스펙에 맞춰 자체적으로 구축함.

RootSet은 삭제되지 않음. UProperty로 “참조”된 객체는 삭제되지 않음. UProperty를 못쓰는 경우 FGCObject클래스를 상속받아서 AddReferencedObject 를 이용한 경우 UProperty참조와 동일.

 

 

클러스터링을 안쓰면 좌측과 같음.

 
 

 

 

 

 

 

https://qiita.com/EGJ-Takashi_Suzuki/items/2c96c4e6f37cc5028207

https://ikrima.dev/ue4guide/engine-programming/memory/tracking-references/


+ 에디터 내에서는 Stat 사용이 가장 중요.

 

 

https://dev.epicgames.com/documentation/en-us/unreal-engine/stat-commands-in-unreal-engine?application_version=5.1 각각의 요소별 설명.

+ https://www.cnblogs.c om/kekec/p/14137774.html 여러가지 dump 커맨드.

'세미나' 카테고리의 다른 글

2차 미분방정식의 물리적 예시  (0) 2025.05.12
공업수학 복소해석학, 복소평면  (0) 2025.05.12
PID제어  (0) 2025.05.12