항목 50 - new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
컴파일러가 기본적으로 제공하는 operator new / delete를 수정하려는 이유가 뭘까?
- 잘못된 힙 사용을 탐지하기 위해
1) new를 통해 할당한 메모리를 두 번 이상 delete하여 해제하는 경우 (double free)
- undefined behavior
2) 데이터 오버런(overrun), 언더런(underrun) 탐지
- 할당된 메모리 블록의 시작/끝을 넘어서 기록한 경우
-- 커스터마이징된 operator new를 활용하여 요구된 크기보다 약간 더 큰 메모리르 할당하여 사용자가 실제로 사용할 메모리의 앞/뒤에 오버런/언더런 탐지용 바이트 패턴(signature)을 적도록 만듦.
- 효율을 향상시키기 위해
operator new / delete 함수는 대체적으로 범용적으로 설계된 것이다.
다양한 경우에 대해서 무난하게 동작하지만, 특정한 경우에 우수하게 동작하지는 않는다. 그렇기 때문에, 프로그래머가 자신의 프로그램이 동적 메모리를 어떤 패턴으로 사용하는지 제대로 이해하고 있다면, operator new / delete를 커스터마이징하여 성능 향상을 기대할 수 있다. 여기서 성능 향상이라 함은, 실행 시간 단축/메모리 공간 절약을 의미 한다. 응용프로그램에 따라서는 new / delete를 커스터마이징하는 것만으로도 엄청난 성능 향상을 기대할 수 있다.
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
new / delete를 커스터마이징하기에 앞서, 소프트웨어가 동적 메모리를 어떻게 사용하는지에 관한 정보를 수집하는 것이 좋음.
- 할당된 메모리 블록의 크기는 어떤 분포를 보이는지
- 각각의 사용 기간은 어떤 분포를 보이는지
- 메모리 할당/해제되는 순서가 FIFO/LIFO/임의의 순서 중 어디에 해당되는지
- 시간 경과에 따라 메모리 사용 패턴 변경 유무
- 각 실행 단계 마다 소프트웨어가 보이는 메모리 할당/해제 패턴이 확연한 차이를 보이는지
- 한 번에 실제 사용되는 동적 할당 메모리의 최대량(high water mark - 최고점)은 어떤지
→ operator new / delete를 사용한다면 이런 정보를 아주 쉽게 수집 가능
example) buffer overrun / underrun을 탐지하는 전역 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); // signature 2개를 앞 뒤에 붙일 수 있을
// 만큼만 메모리 크기를 늘림
void *pMem = malloc(realSize); // malloc을 호출하여
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);
}
위 예제는 malloc을 통해 할당받은 포인터에 int 크기 만큼을 더해서 리턴하므로, 안정성을 보장받지 못한다. 만약 사용자가 double을 담을 메모리를 얻어내는데, 컴퓨터 아키텍처가 8바이트 단위로 align해야 하는 경우 alignment가 깨지기 때문이다.
그래서 operator new / delete를 바닥부터 커스터마이징하기 보다, 상용화된 메모리 관리 함수 및 오픈 소스를 사용하는 것이 좋다.
Q) 기본 제공된 new / delete 를 언제 대체해야 하는가? (전역/클래스 버전 전부 포함)
- 잘못된 힙 사용 탐지
- 동적 할당 메모리의 실제 사용에 관한 통계 정보 수집
- 할당 및 해제 속력을 높이기 위해
class-specific 할당자가 해당
- Thread-Safe한 할당자를 컴파일러에서 제공하는 경우, 단일 스레드로 동작하는 나의 프로그램에서 Thread-Safe 기능이 없는 할당자를 사용함으로써 성능 개선
- 이런 결정을 하기전엔, 프로파일링을 통해 해당 부분이 Bottleneck에 해당되는지 확인 필요
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
범용 메모리 관리자는 커스터마이징된 버전과 비교해서 속력이 느린 경우가 많은데다가, 메모리도 많이 잡아먹는 사례가 많음.
할당된 각각의 메모리 블록에 대해 전체적으로 지우는 부담이 꽤 되기 때문 - 크기가 작은 객체에 대해 튜닝된 할당자를 사용시 이러한 오버헤드 제거 가능
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
x86 아키텍처에선, double이 8바이트 단위로 align되어 있을 때 읽기/쓰기 속도가 가장 빠르지만, 책이 작성될 때 기준 몇몇 컴파일러들은 기본적으로 제공된 operator new 함수가 double에 대한 동적 할당시 8바이트 align을 보장하지 않음
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
한 프로그램에서 특정 자료구조 몇 개가 대개 한 번에 동시에 쓰인다는 것을 프로그래머가 알고 있고, 이들에 대해 Page Fault 횟수를 최소화하고 싶을 경우, 해당 자료구조를 담을 별도의 힙을 생성함으로써 이들이 가능한 적은 Page를 차지하도록 하면 좋은 효과를 볼 수 있음. 이러한 메모리 군집화는 placement new / placement delete를 통해 쉽게 구현 가능
→ 여러 개의 힙 할당된 메모리를 동일한 힙 하나에 모아둠으로써 사용되는 Page 개수를 줄여 Locality 향상을 기대한다는 의미인 듯 하다.
- 그때 그때 원하는 동작을 수행하도록 하기 위해
컴파일러가 제공하는 기본적인 operator new / delete에서 하지 못하는 일을 커스터마이징하여 동작을 변경하고 싶은 경우가 해당. 예를 들어
1) 메모리 할당/해제를 공유 메모리에다 하고 싶은데 공유 메모리를 조작하는 일은 C API로밖에 할 수 없는 경우.
placement new / placement delete를 사용하여 커스터마이징함으로써 기존의 C API에 C++ 동작 추가 가능.
2) 해제한 메모리 블록에 0을 덮어쓰도록 하는 operator delete를 만들어 프로그램의 보안을 강화하는 경우
요약
개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있습니다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함됩니다.
항목 51 - new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자
'컴퓨터공학 > Effective C++' 카테고리의 다른 글
| Return Value Optimization(RVO) - More Effective C++ (0) | 2025.11.13 |
|---|---|
| 11. operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2025.02.13 |
| 10. 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2025.02.04 |
| 9. 객체 생성 및 소멸 과정 중에는 절대 가상 함수를 호출하지 말자 (0) | 2025.02.03 |
| 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2025.02.02 |