성능을 위한 메모리 사용 tip

사실 최적화시 알고리즘보다 중요한 메모리 할당

  • OS의 메모리 매니저의 기본 관리 단위는 페이지(4KB / 1MB / 2MB / 4MB)

그런데 사용자가 1Byte메모리 할당에 4KB를 할당받아 쓰기엔 너무 낭비가 크다

따라서 임의의 덩어리 메모리를 쪼개서 다양한 사이즈의 메모리 블록으로 사용할 수 있도록 하자. -> Heap 사용

Window의 메모리 할당

  • new/malloc -> HeapAlloc() -> VirtualAlloc()

Untitled

Commit하기전 reserver도 수행하는데 C/C++에서 malloc/new 시 에 보통 동시에 이루어짐

Heap 메모리란

  • 큰 덩어리의 메모리부터 다양한 사이즈의의 메모리 블록을 할당.
  • 해제 시 인접한 블록과 병합할 수 있으면 병합하여 더 큰 메모리 블록을 유지.
  • 대부분의 메모리 관리 시스템이 heap based.

Heap의 성능 문제

  • 할당시엔 적합한 사이즈의 블록 찾기
  • 해제 시 병합 비용 최대한 큰 덩어리로 가질 수 있게 병합함
  • 단편화
  • Commit 비용

메모리 사용 특성 고려

  • 특정 사이즈(struct의 사이즈)의 메모리 블록을 왕창 할당할 일이 많다.
  • 게임에서는 메모리 사용량 peak에 도달한 후 다시 peak에 도달할 가능성이 높다(굳이 즉시 해제할 필요가 없다).

따라서 게임에서는 고정 사이즈 메모리풀 추가적으로 사용

  • 사이즈가 고정된다.
  • 적합한 사이즈의 메모리를 탐색할 필요가 없다.
  • 병합이 필요 없다.
  • 단편화가 생기지 않는다.
  • 다양한 사이즈의 메모리를 할당할 수는 없으므로 heap을 완전히 대체할 수는 없다.

조금 더 똑똑하게

  • 최대 블록 개수만큼 모두 할당해둘 필요는 없다.
  • 최대 개수는 넉넉하게. 기본 할당 개수는 적게

Untitled

초기상황 Block #0에 16개 할당되어있음

기존에 비해서 앞에 헤더를 추가한다. 1byte면 충분(align감안해 조금 더 크게)

처음에 0번 할당되었다는걸 인지하기위함

Untitled

Alloc 32번 호출시엔 1번에서 할당해서 반환

48번 할당시

Untitled

windows 같은경우는 low fragmentation heap LFH라는 게 존재

특정사이즈 이하는 LFH로 들어가는데 얘로 작동하면 생각보다 느리지않음

성능테스트

Untitled

1블록 사이즈 1024 바이트 65536 할당/해제할때 걸리는 시간 측정

1024byte는 LFH써서 일반 할당 해제도 생각보다느리지는않음 ticks = ms

Untitled

가변사이즈 테스트 : 차이가 좀 더 벌어짐

Untitled

LFH를 못쓰게 256kb정도로 잡아버리고 할당해제를 해버린 결과

메모리풀 성능도 많이 느려졌는데 Commit 비용

일반적인 할당/해제에는 해제에 시간이 더 많이 들어감

Untitled

메모리 블록 재활용

  • CRT heap 또는 API에서 제공하는 할당 방법을 사용하되 한번 할당한 메모리(리소스)는 재활용한다.
  • 해제 타이밍에 즉시 해제하지 않고 사이즈별로 분류해서 재활용 리스트에 등록해 둔다.
  • 할당 타이밍에 재활용 리스트를 먼저 조회해서 적합한 사이즈의 메모리가 있으면 재활용한다.
  • D3D등 정해진 방법으로 리소스를 할당해야 할 경우 유용하다

여러 번 할당, 한번에 해제

  • 특정 타이밍에 여러 개의 블록을 할당하고 지속적으로 사용되지는 않는 경우.
  • 선형 메모리로부터 순차적으로 할당(포인터 offset만 증가)한다.
  • 더 이상 어떤 메모리도 사용되지 않을 때 선형 메모리를 통째로 해제한다.
  • Tree빌드 등에 유용하다

On demand 배열

  • 거대한 배열이 필요할 때
    • 그러나 그 배열의 일부만 사용되고
    • 어느 영역이 사용될지 알 수 없다.
  • NxNxN씩 잘라서 배열 그룹을 만든다.
  • 각 배열그룹 내의 메모리는 할당하지 않는다
  • Access(), Alloc()등의 함수로 요청이 들어오면 그때 할당하고 포인터를 돌려준다.
  • Paging기법과 유사
  • 월드 공간을 그리드로 구현할 때 유용함

Untitled

거대한 2차원 공간에

Untitled

8x8로 sector를 나눈다고 생각하자

우리는 이 섹터를 캐싱해서 들고있자.

Untitled

0,0에 대해서 필요할때 해당 섹터에 대해서 메모리를 받아와서(할당) 사용

Untitled

Untitled

일반적인 tip

작업용 임시 메모리

  • Stack(로컬변수)는 대체로 cache hit한다. 따라서 작업용 메모리는 stack을 사용하는 편이 성능상 유리하다. 8byte짜리 32개 배열 정도는 문제X
  • 그러나 stack 메모리를 너무 크게 잡으면 cache miss할 가능성이 높고 stack메모리를 추가 commit하느라 오히려 느려진다.
  • 너무 큰 stack 변수(배열포함)를 사용하면 VC++에서 경고를 해준다.
  • 큰 사이즈의 working 메모리가 필요할 경우 heap에다 잡아두고 사용한다.

멀티스레드 상황

  • 스레드가 사용할 working메모리는 한번에 할당해서 배정하는 것이 가장 좋다.
  • 메모리풀을 만들어 사용할 경우 lock이 필요없도록 아예 별도의 메모리 풀을 만들어서 배정한다. 스레드별로 메모리풀을 따로 준다.
  • 스레드마다 heap을 사용할 필요가 있을 경우 별도의 heap을 만들어서 배정한다

부지불식간에 사용중인 동적할당 제거

  • STL등 자료구조에서 new/delete를 사용할 경우 빈번하게 호출되고 있지 않은지 확인할것.
  • 메모리풀 만들때 다른 자료구조 갖다 쓰려고 한다면 각별히 주의할것. 그 자료구조에서 메모리 할당/해제로 CPU자원을 낭비할 수 있다.
  • 함수 안에서 String 클래스등 사용 주의.
  • 렌더링 프레임에 malloc/free/new/delete 호출이 없도록 한다.
  • 로딩이 느리다면 I/O비용을 의심하기 전에 메모리 할당/해제 비용부터 확인할 것

자투리

메모리릭 잡을때 사용 CRT Heap Check

Untitled

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/crtsetdbgflag?view=msvc-170

이것만 써도 거의 윈도우즈는 다 잡음

자체적으로 만든 메모리풀은 자체적으로 누수 체크 만들어야함 만들 때 같이 만들기

windbg 들고 힙 덤프뜨면됨

CRT힙에서 할당할때 콜스택 저장

소스코드 라인까지 같이 저장

오버로딩된것

스냅샷이 제일 좋음

메모리 할당

  • 게임 루프 안에서 할당 해제
    • 고정사이즈 메모리풀
  • 초기화/종료 처리등등 게임루프 밖에서 단발성 할당
    • malloc new → HeapAlloc
  • 가변길이 쓸 경우
    • gpu 메모리를 맵핑해야하는 경우
    • 병합기능이 들어갈 경우 OS Heap 보다 잘 만들 수 없음

힙 함수자체는 시스템콜이아님 유저모드임 시스템콜 할 수도있음

릭 잡는 매크로 원리

**AddressSanitizer**

https://learn.microsoft.com/ko-kr/cpp/sanitizers/asan?view=msvc-170

원리 메모리 영역 복사본을 하나 항상 더 잡는다

shadow copy 써 넣어보고 깨먹나 안깨먹나 검사

https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm

CRT Heap체크는 런타임에 무리없음

_CrtCheckMemory() 등으로 체크가능

호출시에만 느림

프로세스가 생성될때 기본적으로 CRT Heap을 하나 만들어줌

new malloc 시 해당 힙에서 가져옴

Untitled

뉴/ 메모리 사용시 앞단에 해당 코드 필요

Posted 2023-10-06