Deeper Learning

[WinAPI] 메모리 누수 디버깅 (Heap Profiling) 본문

Game Development/WinAPI

[WinAPI] 메모리 누수 디버깅 (Heap Profiling)

Dlaiml 2024. 6. 13. 17:39

WinAPI 프로젝트에 애니메이션을 넣고 플레이어의 상태를 정의하는 코드를 추가하여 공격, 이동, 유휴 애니메이션이 재생되도록 하여 조금 더 게임의 형태를 갖추게 되었다.

공격속도에 따라 애니메이션의 빠르기가 달라지도록 하였고 투사체가 발사되기에 자연스러운 애니메이션 프레임에 Bullet Object를 Spawn 하도록 코드를 추가하였다.

 

 

게임을 켜두고 문제를 해결하기 위해 코딩을 오랜 시간 하다가 다시 게임을 들어가 보니 프레임이 체감될 정도로 떨어진 것을 확인하였다. 작업을 위해 새로운 씬을 세팅한 이후 메모리를 모니터링한 적이 없어 Memory Leak이 의심되었고 Visual studio의 진단도구의 Heap Profiling을 사용하여 이를 해결하였다.

 

 

진단도구 - Diagnostic Tools

 

 

Visual Studio의 진단 도구이다. 상단에는 Breakpoint Event 시점, Memory, CPU 사용량을 시계열로 보여주고 하단에는 상단 항목의 상세 정보들을 볼 수 있다.

게임에서 아무 업데이트 항목이 없고 저장하는 항목 또한 없음에도 Process Memory가 계속 증가하고 있음을 볼 수 있다.

 

 

Memory Usage에서 Take Snapshot으로 스냅샷시점의 Heap 메모리 정보를 기록할 수 있다. 실행 초기(0.33s)에 찍은 스냅샷과 34.08s에 찍은 스냅샷을 보면 할당 횟수가 크게 증가하였고 사용하고 있는 Heap Size도 매우 커진 것을 알 수 있다.

 

 

Allocation 횟수를 클릭하면 Native Memory 정보를 확인할 수 있는데, Heap에 할당된 객체를 Object Type과 횟수, 크기까지 모두 기록되어 있다.

 

iterator의 디버깅을 위해 호출되는 std::_Container_proxy를 제외하고 보면 타일 오브젝트가 가장 큰 공간을 차지하고 있음을 알 수 있다. 문제가 타일에 있을 수 있다는 정보를 얻었기 때문에 다시 10초 정도 더 프로그램을 실행시키고 스냅샷을 찍어보았다.

 

 

이번에는 증감(Diff) 부분을 클릭하여 이전 스냅샷과의 차이를 확인하였다.

 

 

한 프레임이 끝나면서 오브젝트 생성/삭제를 처리하기 직전 시점에 동일하게 Breakpoint를 걸었음에도 할당된 메모리의 수가 크게 늘어났다. 이는 메모리가 정상적으로 해제되지 않아 생기는 Memory leak 문제이다.

 

Object type을 클릭하면 해당하는 Instance들이 쭉 나오고 Instance를 클릭하면 아래 Allocation Call Stack을 볼 수 있다.

 

이렇게 CreateTile 함수에서 생성된 타일 오브젝트들이 정상적으로 메모리에서 해제되지 않는 문제가 있다는 것까지 확인하였다.

// CreateTile Pseudo Code

/*DeleteGroup(GROUP_TYPE::TILE);
for (UINT j = 0; j < _iXCount; ++j)
{
	CTile* pTile = new CTile;
	pTile->SetPos(Vec2((float)(j * TILE_SIZE), (float)(i * TILE_SIZE)));
	pTile->SetTexture(pTileTex);
	AddObject(pTile, GROUP_TYPE::TILE);
}*/

 

CreateTile은 DeleteGroup으로 타일 타입의 Object를 모두 제거하고 타일을 다시 생성하는 함수로 new를 사용하여 heap 메모리에 동적할당하는 코드가 포함되어 있다.

 

AddObject로 이를 Scene 전체 Object를 관리하는 벡터에 넣어두었다가 DeleteGroup에서 이를 delete 하기 때문에 DeleteGroup 함수에 문제가 있을 확률이 높다.

 

4번째 스냅샷은 DeleteGroup 함수가 실행된 직후 시점에 찍은 메모리 정보이다. 타일들이 모두 해제되고 다시 생성되어야 하는데 할당 수 증감이 1밖에 없는 것을 확인할 수 있다.

 

타일을 지울 때 사용하는 DeleteGroup함수는 타일 타입의 오브젝트의 포인터가 저장된 벡터를 input으로 하는 DeleteVectorSafe 함수를 호출하는 코드가 전부이다. 

 

template<typename T>
void DeleteVectorSafe(vector<T>& _vec)
{
	for (size_t i = 0; i < _vec.size(); i++)
	{
		if (nullptr != _vec[i])
		{
			delete _vec[i];
		}
		_vec.clear();
	}
}

 

DeleteVectorSafe를 보면 index를 순회하면서 포인터가 가리키는 메모리를 해제한다. 

vector를 clear하는 부분이 for문 내부에 있어 index를 전부 순회하기 전에 vector가 clear 된다.

vector 내부 원소의 자료형이 Object* 로 포인터이기 때문에 포인터가 가리키는 객체는 메모리에서 해제되지 않고 소멸자도 호출되지 않는다. 

 

이렇게 매 프레임 (타일 수 - 1) 만큼 해제되지 않고 메모리 누수가 일어나면서 프레임 드랍이 일어났던 것이다.

_vec.clear() 코드를 for문 밖으로 빼서 문제를 해결한 후 다시 진단도구를 통해 메모리를 확인하였다.

 

이전과 다르게 Process Memory가 시간이 지나면서 점점 증가하지 않는 것을 확인할 수 있다.

 

CRT 라이브러리 

CRT 라이브러리를 사용하여도 쉽게 메모리 누수를 잡을 수 있다.

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); // CRT Flag
CAnimation* pLeak = new CAnimation; // Memory Leak Occured

 

_CrtSetDbgFlag를 메인함수의 가장 앞에 추가한다. 사용한 두 플래그의 기능은 아래와 같다.

_CRTDBG_ALLOC_MEM_DF: 디버그에서 메모리 할당이 일어날 때 추적 활성화

_CRTDBG_LEAK_CHECK_DF: 디버그에서 프로그램 종료시 메모리 누수 검사 수행 

 

그리고 Player 클래스에 할당해제 하지 않는 힙 메모리 동적 할당 테스트 코드를 삽입하였다. 

 

 

프로그램을 종료하면 위처럼 brace 안에 숫자가 나오고 우측엔 문제가 생긴 메모리의 주소와 데이터가 출력된다. 

_CrtSetBreakAlloc(426);

 

앞에서 확인한 숫자를 _CrtSetBreakAlloc()의 매개변수로 입력하고 메인함수 진입 부분에 넣은 뒤 디버거를 다시 실행한다. 메모리 릭이 생긴 코드에서 자동으로 Break가 걸리며 Call Stack을 확인해서 문제가 생긴 부분을 확인할 수 있다.

 

 

 

Comments