Advanced C++

[C++] 메모리 관리 (2) 힙 메모리 new/malloc/HeapAlloc/VirtualAlloc

로파이 2021. 10. 3. 19:14

동적 메모리를 사용하는 이유 - 스택과 힙

프로그래머가 사용할 수 있는 메모리는 데이터 세그먼트, 스택 그리고 힙 영역을 사용할 수 있다. 데이터 세그먼트 영역은 정적 변수를 위한 메모리이기 때문에 주 사용 메모리라고 볼 수 없고 주로 스택과 힙 영역의 메모리를 다루게 된다.

 

스택 메모리는 함수를 위한 임시 메모리라고 할 수 있다. 실행 흐름에 쓰이는 모든 지역(자동) 변수와 함수 인자, 함수 호출 전 반환 값등을 위해 쓰이며 이 과정이 실행 흐름에서 스택 포인터를 따라 메모리를 차곡 차곡 쌓아가는 모습 때문에 스택이라고 부른다.

 

컴파일된 실제 어셈블리 코드를 보면 함수 실행 문맥에서 스택 메모리는 레지스터와 함께 값을 저장하기 위해 쓰이며 함수 종료시 현재 스택 포인터 레지스터에 프레임 포인터 레지스터(현재 실행에서 쓰이는 스택 메모리의 시작지점)의 값으로 되돌리는 것으로 메모리를 정리한다.

#include <iostream>

void func()
{
	int a = 1;
	int b = 2;
	int c = a + b;
}

int main()
{
	int local = 0;

	func();

	return 0;
}

func() 함수의 return 직전에 수행되는 에필로그 (디버그-창-디스어셈블리)     스택 포인터에 프레임 시작 주소를 대입하고 있다.

스택의 경우 컴파일 시점에서 모든 함수의 흐름이 파악되고 얼만큼의 메모리가 어느 라인에서 필요한 지 파악되기 때문에 정적이라고 할 수 있다. 힙 메모리의 경우 동적 메모리라고 하며 실행 시점에서 프로그래머가 new/malloc과 같은 방법으로 운영체제에게 고정된 사이즈만큼의 메모리를 힙에 할당하고 그 공간의 시작 주소를 요청하게 된다.

 

스택 vs 힙 빠르기 비교

스택 메모리 스택 포인터 값만 바꾸고 함수를 종료하기 때문에 단순하고 빠르다. 반면 힙 메모리는 malloc/new를 통해 메모리를 할당에서 부터 커널을 통해 힙 메모리 공간 사용을 요청을 하고 요청한 만큼의 메모리에 대한 주소를 반환해준다. 요청 시 만약 커밋된 힙 공간이 충분하지 않다면 일정 가상 메모리로부터 물리 메모리로 등록 과정을 거쳐 커밋된 큰 힙 블록을 마련해야 한다. 반대로 free/delete를 통해 메모리를 반납한 경우 커널 내부에서는 큰 블록에 해당하는 커밋된 힙 공간에서 합쳐질 수 있는 힙 블록들을 다시 모아 하나의 블록으로 만드는 과정이 수반될 수 있다. 이러한 과정이 힙 메모리를 사용하는 것을 느리게 만든다.

 

C++ new와 malloc

new의 경우 C에서 다형성 클래스를 지원하기 위해 도입된 동적 메모리 할당 방법이다. new의 경우 operator로 임의의 타입에 대하여 동적 메모리를 할당하고 해당 타입의 생성자를 호출하는 기능을 지닌다. C++의 클래스 지원으로 가상 함수를 위한 가상 함수 테이블 생성 등의 기능을 new를 통해 지원한다. malloc의 경우 C에서 호출하는 동적 메모리 할당 방식으로 정해진 사이즈만큼 요청하면 주소를 얻게 되는 C의 표준 함수이다. new의 경우도 내부적으로 malloc을 호출한다.

 

HeapAlloc

윈도우 OS에서 new/malloc는 HeapAlloc를 사용한다.

https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapalloc

DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
  HANDLE hHeap,
  DWORD  dwFlags,
  SIZE_T dwBytes
);
  • hHeap : HeapCreate 혹은 GetProcessHeap 함수를 통해 얻은 커밋된 힙 공간에 대한 핸들
  • dwFlags : 힙 할당을 할 때 지정할 행동에 대한 옵션이다. 예외를 던지거나 0으로 메모리를 초기화하는 옵션을 지정할 수 있다.
  • dwBytes : 실제 필요한 메모리 사이즈
  • 메모리 반납 : HeapFree 함수를 통해 이루어진다.

- HeapAlloc에 dwFlags =  0을 지정하면 기본 동작으로 메모리를 할당하며 HEAP_NO_SERIALIZE를 지정하지 않는 이상 동적 메모리 할당은 동기화 기능으로 Mutual Exclusion을 보장한다. Windows OS에서 new/malloc은 이 기능을 사용하여 thread-safe한 것으로 알려져 있다. 하지만, OS마다 다르게 구현되어 있고 컴파일러 버전에 따라 다르게 동작할 수 있으니 Portable한 코드를 작성하려면 thread-safe에 대해 생각해볼 필요가 있다.

 

VirtualAlloc

https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc

물리 메모리로 커밋된 힙 메모리가 부족할 경우 HeapAlloc은 VirtualAlloc이라는 system call을 사용하여 커밋된 공간을 얻어와야한다. VirtualAlloc 함수는 현재 프로세스가 사용하는 가상 주소 공간의 페이지 상태를 예약/커밋/다른 상태로 변경한다.

LPVOID VirtualAlloc(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

lpAddress : 메모리를 할당할 영역의 시작 주소를 가리킨다. 메모리가 예약된 상태라면 Allocation Granularity 크기의 배수 중 가장 가까운 주소로 반올림된다. 메모리가 예약되고 커밋된 상태라면 다음 페이지 경계로 반올림 된다. Allcoation Granularity와 페이지 크기를 알고 싶다면, GetSystemInfo 함수를 이용하라. null을 전달하면 시스템이 알아서 결정한다.

dwSize : 영역의 사이즈, lpAddress가 null이라면 다음 페이지 경계의 반올림 값으로 설정된다.

flAllocationType : 변경할 메모리 상태 타입을 말한다. 커밋/예약/리셋/리셋취소 등을 지정할 수 있다.

flProtect : 메모리 보호관련 하여 접근 권한을 설정한다. 

 

- VirtualAlloc은 예약된 페이지를 다시 예약할 수 없으나 커밋된 페이지를 커밋할 수는 있다.

 

WinAPI에서 사용되는 동적 할당 호출 다이어그램

https://stackoverflow.com/questions/872072/whats-the-differences-between-virtualalloc-and-heapalloc