50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
컴파일러의 기본 operator new와 operator delete를 바꾸고 싶은 가장 흔한 이유 세 가지를 보면 다음과 같다.
- 잘못된 힙 사용을 탐지하기 위해
- new한 메모리에 delete를 잊으면 메모리가 누출된다. 두 번 이상 delete하면 미정의 동작이 발생한다. 만일 할당된 메모리 주소의 목록을 operator new가 유지해 두고 operator delete가 그 목록으로부터 주소를 하나씩 제거해 주게 만들어져 있다면, 이런 식의 실수는 쉽게 잡아낼 수 있을 것이다.
- 데이터 오버런(overrun) 및 언더런(underrun)을 대비하여 operator new를 활용한다면, 요구된 크기보다 약간 더 메모리를 할당한 후에 사용자가 실제로 사용할 메모리의 앞과 뒤에 오버런/언더런 탐지용 바이트 패턴(일명 "경계표지(signature))을 적어두도록 만들 수 있을 것이다.
- 효율을 향상시키기 위해
- 기본 버전의 operator new 및 operator delete 함수는 대체적으로 일반적인 쓰임새에 맞추어 설계된 것이다. 응용 프로그램마다 메모리 관리자에 대한 요구사항은 정말 가지각색이기 때문이다. 만일 개발자가 자신의 프로그램이 동적 메모리를 어떤 성향으로 사용하는지를 제대로 이해하고 있다면, 사용자 정의 operator new와 operator delete를 자신이 만들어 쓰는 편이 기본제공 버전을 썼을 때보다 더 우수한 성능을 낼 확률이 높다.
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 소프트웨어가 동적 메모리를 어떻게 사용하는지에 관한 정보를 수집하는 목적으로 operator new 및 operator delete를 사용할 수 있다.
버퍼 오버런 및 언더런을 탐지하는 사용자 정의 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); // 경계표지 2개를 앞뒤로 붙일 수 있을 만큼의 메모리 크기
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에서 다룬다. 이 함수는 다른 문제가 있는데, 바로 바이트 정렬(alignment)이다.
우리가 사용하는 아키텍처는 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다. 이런 바이트 정렬 문제는 지금 경우에도 아주 중요하다. 왜냐하면, 모든 operator new 함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다는 것이 C++의 요구사항이기 때문이다. 위의 함수는 int 크기만큼 뒤로 어긋난 주소를 포인터로 반환하고 있다. 이렇게 되는 경우는 안전하다는 보장을 할 수가 없다.
정말 잘 돌아가는 쓸 만한 메모리 관리자를 만드는 것은 정말 어렵다. 지극히 일반적인 경우만으로 말한다면, 꼭 만들어 쓸 이유가 없다면 굳이 들이댈 필요는 없다. 꽤 많은 플랫폼에 쓸 수 있는 메모리 관리 함수만을 전문적으로 다루는 상업용 제품도 출시되어 있고, 오픈 소스도 나와 있다. 예를 들어, 부스트(boost)의 풀(Pool) 라이브러리는 오픈 소스 메모리 할당자 중 하나인데 크기가 작은 객체(소형 객체)를 많이 할당할 경우에 좋다.
이제 '언제' new 및 delete의 기본제공 버전을 다른 것으로 대체하는 작업을 해야 의미가 있는지 알아보자.
- 잘못된 힙 사용을 탐지하기 위해 (앞에서 나온 내용)
- 동적 할당메모리의 실제 사용에 관한 통계 정보를 수집하기 위해 (앞에서 나온 내용)
- 할당 및 해제 속력을 높이기 위해
- 기본으로 제공되는 범용 할당자는 사용자 정의 버전보다 꽤 느린 경우가 적지 않다.
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 범용 메모리 관리자는 메모리도 많이 잡아먹는 사례가 허다하다.
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
- 아키텍처의 바이트 정렬 동작을 만족하도록, 기본제공 operator new를 사용자 정의 버전으로 바꿈으로써 프로그램 수행 성능을 확 끌어올릴 수 있다.
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
- 특정 자료구조 몇 개가 대개 한 번에 동시에 쓰이고 있다는 사실을 알고 있고, 앞으로 이들에 대해서는 페이지 부재(page fault) 발생 횟수를 최소화하고 싶을 경우, 해당 자료구조를 담을 별도의 힙을 생성함으로써 이들이 가능한 적은 페이지를 차지하도록 하면 상당히 좋은 효과를 볼 수 있다.(메모리 군집화).
- 그때그때 원하는 동작을 수행하도록 하기 위해
- 컴파일러가 주는 버전이 하지 못하는 일을 operator new 및 operator delete가 해주었으면 하고 바라는 때가 종종 있다.
이것만은 잊지 말자!
- 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함된다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자 (0) | 2023.11.24 |
---|---|
[Effective C++] 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (0) | 2023.11.22 |
[Effective C++] 49: new 처리자의 동작 원리를 제대로 이해하자 (1) | 2023.11.21 |
[Effective C++] 48: 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2023.11.18 |
[Effective C++] 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2023.11.18 |