- 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 이유가 있다. 여기에는 수행 성능 향상, 힙 사용 시의 에러 디버깅, 힙 사용 정보 수집 등의 목적이 있다.
new와 delete
operator new와 operator delete를 바꾸는 목적은 여러 가지가 있다.
가장 흔한 이유는 다음과 같다.
- 잘못된 힙 사용을 탐지하기 위해
new한 메모리에 delete를 하는 것을 잊어버리면 메모리가 누출된다.
한 번 new한 메모리를 두 번 이상 delete하면 미정의 동작이 발생한다.
만일 할당된 메모리 주소의 목록을 operator new가 유지해 두고 operator delete가 그 목록으로부터 주소를 하나씩 제거해 주게 만들어져 있다면, 이런 실수는 쉽게 잡아낼 수 있다.
또한, 데이터 오버런(할당된 메모리를 넘어서 기록) 및 언더런(할당된 메모리 앞에 기록)이 발생할 수 있다.
이런 경우에 대비하여 사용자 정의 operator new를 활용한다면, 요구된 크기보다 메모리를 할당한 후에 사용자가 실제로 사용할 메모리의 앞과 뒤에 오버런/언더런 탐지용 바이트 패턴을 적어두도록 만들 수 있다.
이를 경계표지라고 하며, operator delete에서 경계표지를 점검하면 된다.
- 효율을 향상시키기 위해
컴파일러가 제공하는 기본 버전의 operator new 및 operator delete 함수는 대체적으로는 일반적인 쓰임새에 맞추어 설계된 것이다. 실행 기간이 짧지 않은 프로그램에서 잘 돌아가야 하며, 1초 안에 끝나는 프로그램에서도 별 문제가 없어야 한다.
할당한 블록의 크기와 상관없이 메모리 할당 요청을 무난하게 처리해야 한다.
또한, 수명과도 상관없이 동작해야 하며 힙 단편화에 대한 대처방안도 없으면 안 된다.
이렇듯 메모리 관리자에 대한 요구사항은 다양하다.
즉, 기본 버전의 operator new와 operator delete는 보편적인 상황에 대처하기 위한 동작이므로 효율을 높이기 위해 operator new와 operator delete를 만들어 낼 수 있다.
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
프로그램이 동적 메모리를 어떻게 사용하는지에 관한 정보를 수집하고 싶을 수 있다.
실행 단계마다 프로그램이 보이는 메모리 할당/해제 패턴과 동적 할당 메모리의 최대량 등을 알기 위해 operator new와 operator delete를 만들 수 있다.
operator new 제작 예시
개념적으로 보면 operator new를 직접 만드는 작업은 어렵지 않다.
예로 오버런과 언더런을 탐지하는 전역 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);
}
위의 코드는 고쳐야 할 부분이 있다.
operator new를 제작할 때 관례를 지키지 않은 부분을 고쳐야 한다.
operator new에는 new 처리자 함수를 호출하는 루프가 반드시 들어 있어야 하는데 그런 부분이 없다.
그리고 바이트 정렬 문제가 있다.
컴퓨터 아키텍처의 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다.
이를테면, 포인터는 4의 배수에 해당하는 주소에 맞추어 저장되어야 하거나 double 값은 8의 배수에 해당하는 주소에 맞추어 저장되어야 한다.
어떤 아키텍처의 경우에는 이 바이트 정렬 제약을 따르지 않으면 프로그램이 실행되다가 하드웨어 예외를 일으킬 수 있다.
또한, 바이트 정렬이 성능에도 영향을 미칠 수 있다.
바이트 정렬 문제는 지금 경우에도 매우 중요하다.
왜냐하면, 모든 operator new 함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 하는 요구사항 때문이다.
표준 malloc 함수는 이 요구사항에 맞추어 구현되어 있다.
하지만 malloc에서 얻은 포인터를 operator new가 바로 반환하지 않는다.
그 포인터를 기준으로 int 크기만큼 뒤로 어긋난 주소를 포인터로 반환하고 있다.
이렇게 되는 경우는 안전을 보장할 수 없다.
만일 사용자가 operator new를 호출해서 double을 담을 메모리를 얻어내는데, int는 4바이트이고 double은 8바이트 단위로 정렬되어야 하는 컴퓨터에서 그 사용자의 프로그램이 실행되고 있다면, 이것 때문에 프로그램이 다운될 수 있다.
바이트 정렬 등의 문제를 어떻게 다루느냐에 따라 메모리 관리자가 달라진다.
좋은 품질의 메모리 관리자를 만드는 것은 쉽지 않다.
하지만, 꼭 만들어 쓸 이유가 없다면 굳이 만들 필요가 없다.
시중에 나와 있는 컴파일러 중에는 메모리 관리 함수에 디버깅 및 로깅 기능을 넣어 놓고 필요에 따라 전환할 수 있도록 만든 것들도 있다.
또한, 오픈 소스로도 메모리 관리자 패키지가 많이 공개되어 있다.
그중 하나는 부스트의 풀 라이브러리이다.
이 라이브러리에서 제공하는 메모리 할당자는 크기가 작은 객체를 많이 할당할 경우 사용자 정의 메모리 관리 루틴으로 도움을 얻을 수 있다.
또 다른 new & delete 제작 목적
- 잘못된 힙 사용을 탐지하기 위해
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속도를 높이기 위해
기본으로 제공되는 범용 할당자는 사용자 정의 버전보다 꽤 느린 경우가 많다.
특히 사용자 정의 버전이 특정 타입의 객체에 맞추어 설계되어 있으면 더욱 그렇다
부스트의 Pool 라이브러리에서 제공하는 할당자처럼 고정된 크기의 객체만 만들어 주는 할당자의 전형적인 응용 예가 바로 클래스 전용 할당자이다.
응용프로그램은 단일 스레드로 동작하는데 컴파일러에서 기본으로 제공하는 메모리 관리 루틴이 다중스레드에 맞게 만들어져 있다면, 스레드 안정성이 없는 할당자를 직접 만들어 사용함으로써 속도를 높일 수 있다.
물론 적절한 프로파일링을 통해 프로그램 안에서 병목을 일으키는 원인인지 파악하는 것이 먼저이다.
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
범용 메모리 관리자는 사용자 정의 버전과 비교해서 속력이 느린 경우도 많은 데다가 메모리도 많이 잡아먹는 경우가 많다.
할당된 각각의 메모리 블록에 대해 전체적으로 지우는 부담이 꽤 되기 때문이다.
크기가 작은 객체에 대해 튜닝된 할당자를 사용하면 이러한 오버헤드를 실질적으로 제거할 수 있다.
- 기본 할당자의 바이트 정렬 동작을 보장하기 위해
x86 아키텍처에서는 double이 8바이트 단위로 정렬되어 있을 때 읽기, 쓰기 속도가 가장 빠르다.
시중에 나와 있는 컴파일러 중에는 기본적으로 제공되는 operator new 함수가 double에 대한 동적 할당 시에 8바이트 정렬을 보장하지 않는 것들이 있다.
이런 컴파일러를 쓰면, 기본제공 operator new 대신에 8바이트 정렬을 보장하는 사용자 정의 버전으로 바꿈으로써 프로그램 수행 성능을 끌어올릴 수 있다.
- 임의의 관계를 맺고 있는 객체들을 모아 놓기 위해
한 프로그램에서 특정 자료구조 몇 개가 한 번에 쓰이고 있다는 사실을 알고 있고, 앞으로 이들에 대해서는 페이지 부재 발생 횟수를 최소화하고 싶을 경우, 해당 자료구조를 담을 별도의 힙을 생성함으로써 이들이 적은 페이지를 차지하도록 하면 상당히 좋은 효과를 볼 수 있다.
이러한 메모리 군집화는 위치지정 new 및 위치지정 delete를 통해 쉽게 구현할 수 있다.
- 원하는 동작을 수행하도록 하기 위해
기본 버전이 하지 못하는 일을 operator new 및 operator delete가 처리하게 하고 싶은 경우가 있다.
메모리 할당과 해제를 공유 메모리에다 하고 싶은데 공유 메모리를 조작하는 일은 C API로밖에 할 수 없을 때가 한 가지 예이다.
이때 사용자 정의 버전을 만드는 것이다. 위치지정 new와 위치지정 delete가 적당하다.
기존의 C API를 C++에서 적용하는 또 다른 예로는 데이터의 보안 강화를 위해 해제한 메모리 블록에 0을 덮어쓰는 사용자 정의 operator delete를 만드는 경우도 있다.