본문 바로가기
서적 정리/Effective C++

50.new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자

by 민돌이2 2022. 1. 1.

제목만 보고 뭔 소린가 싶다. 동적 할당을 바꿔야 할 필요가 왜 있는가?

 

사용자 정의 operator new

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc)
{
	using namespace std;
	size_t realSize = size + 2 * sizeof(int);

	void* pMem = malloc(realSize);
	if (!pMem) throw bad_alloc();

	*(static_cast<int*>(pMem)) = signature;
	*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;

	return static_cast<Byte*>(pMem) + sizeof(int);
}

위 코드 자체는 틀린 점이 존재한다(51장 참고).

대부분의 컴퓨터는 아키텍처(architecture)적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다. 예를들어 포인터는 4의 배수에 해당하는 주소에 맞추어 저장되어야 하고, double 값은 8의 배수에 해당하는 주소에 맞추어 저장되어야 한다. 이 규칙을 바이트 정렬(alignment)이라고 한다.

 

모든 operator new 함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다는 것이 C++의 요구사항이다. 표준 malloc 함수는 이 요구사항에 맞추어 구현되어 있기에 malloc에서 얻은 포인터를 operator new가 바로 반환하는 것은 안전하다. 하지만 위의 코드는 malloc에서 나온 포인터를 반환하지 않는다. 결론적으로 안전하다는 보장을 할 수 없다.

 

 

사용자정의 new와 delete를 고려해야 할 때

1.잘못된 힙 사용을 탐지하기 위해

new한 메모리에 delete 하는 것을 잊어 버리면 메모리 누출이 되고, 이미 메모리 해제한 메모리를 또 메모리 해제한다면 프로그램은 터질것이다. 만약 할당된 메모리 주소의 목록을 operator new가 유치해 두고 operator delete가 그 목록으로부터 주소를 하나씩 제거해 주게 만들어져 있다면, 이런 실수는 예방할 수 있다.

실수를 하다보면 데이터 오버런(overrun)과 데이터 언더런(underrun)(*주 1)이 발생할 수 있다. 

 

2.효율을 향상시키기 위해

컴파일러가 제공하는 operator new와 operator delete 함수는 일반적인 쓰임새에 맞춰 설계된 것이다. 

일반적인 쓰임새에 초점을 맞췄기에 사용자에 최적화 됐다고는 할 수 없다. new와 delete를 사용자 정의 버전으로 바꾸는 것만으로 성능 향상을 기대해 볼 수 있다.

 

3.동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해

사용자 정의 operator new와 operator delete를 사용하면 메모리가 할당되고 해제되는 순서(FIFO, LIFO), 메모리 분포 등 이런 정보를 아주 쉽게 수집할 수 있다.

 

4.기본 메모리 관리자의 공간 오버헤드를 줄이기 위해

범용 메모리 관리자는 사용자 정의 버전과 비교해서 속력이 느린 경우도 많고, 메모리도 많이 잡아먹는 사례가 많다. 할당된 각각의 메모리 블록에 대해 전체적으로 지우는 부담이 꽤 되기 때문이다. 크기가 작은 객체에 대해 튜닝된 할당자를 사용하면 오버헤드를 실질적으로 제거할 수 있다.

 

5.적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해

64bit 아키텍처에서는 double이 8byte 단위로 정렬되어 있을 때 읽기, 쓰기 속도가 가장 빠르다. 하지만 소수 몇몇 컴파일러는 operator new 함수가 double에 대한 동적 할당 시에 8byte 정렬을 보장하지 않는다. 이런 경우 기존 operator new 대신 8byte 정렬을 보장하는 사용자 정의 버전으로 바꾸면 성능을 올릴 수 있다.

 

6.임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해

페이지 부재(page fault) 발생 횟수를 최소화하고 싶을 때, 해당 자료구조를 담을 별도의 힙을 생성함으로써 이들이 가능한 한 적은 페이지를 차지하도록 하면 효과를 볼 수 있다. 메모리 군집화를 new 및 위치지정 delete를 통해 쉽게 구현할 수 있다.

 

7.그때그때 원하는 동작을 수행하도록 하기 위해

기존 컴파일러의 operator new와 operator delete가 하지 않는 일을 해주었으면 할 때 사용할 수 있다. 예를 들면 메모리 할당과 해제를 공유 메모리에다가 하고 싶은데 공유 메모리를 조작하는 일은 C API로 밖에 할 수 없을 때 사용자 정의 버전을 만들면 가능하다(52장 참고).

 

 

이것만은 잊지 말자

개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함된다.

 

(*주 1) 데이터 오버런 vs 데이터 언더런

오버런 : 할당된 메모리 블록의 끝을 넘어 뒤에 메모리를 할당하는 것

언더런 : 할당된 메모리 블록의 시작을 넘어 앞에 메모리를 할당하는 것

요구된 크기보다 약간 더 메모리를 할당한 후에 사용자가 실제로 사용할 메모리의 앞과 뒤에 오버런/언더런 탐지용 바이트 패턴을 적어두도록 만들면 오버런과 언더런을 예방할 수 있다.

728x90

댓글