이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 360p ~ 369p
이펙티브 C++(360p ~ 369p)
요약
항목 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
- 개념적으로 보면
operator new
를 직접 만드는 것은 어려운 작업이 아니다. - 다음은 버퍼 오버런 및 버퍼 언더런을 탐지하기 쉬운 형태로 만들어 주는 전역
operator new
이다.
static const int signature = 0xDEAFBEEF;
typedef unsigned char Byte;
// 이 코드는 고쳐야 할 부분이 몇 개 있다.
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
// 경계표지 2개를 앞뒤에 붙일 수 있을만큼만 메모리 크기를 늘림
size_t realSize = size + 2 * sizeof(int);
// malloc을 호출하여 실제 메모리를 얻어냄
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
함수를 만들 때 통상적으로 쓰이는 관례를 지키지 않은 데 있다.- 예를 들어
operator new
에는 new 처리자 함수를 호출하는 루프가 반드시 들어 있어야 하는데, 지금의 함수에는 루프가 없는 것이 그런 점들 중 하나다. - 이런 관례는 항목 51에 몰아두었다.
- 예를 들어
- 이 함수에는 좀더 까다로운 문제가 숨어 있다. 바로 바이트 정렬이다.
- 컴퓨터는 많은 경우에 있어서 아키텍처적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다.
- 이를테면 포인터는 4의 배수에 해당하는 주소에 맞추어 저장되어야 하거나 double 값은 8의 배수에 해당하는 주소에 맞추어 저장되어야 한다.
- 어떤 아키텍처의 경우에는 이 바이트 정렬 제약을 따르지 않으면 프로그램이 실행되다가 하드웨어 예외를 일으킬 수 있다.
- 느슨한 제약을 두는 아키텍처의 경우 바이트 정렬을 만족했을 경우에 더 나은 성능을 제공한다.(ex. 인텔 x86 아키텍처)
- 모든
operator new
함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다는 것이 C++의 요구사항이다.- 표준
malloc
함수는 이 요구사항에 맞추어 구현되어 있기 때문에,malloc
에서 얻은 포인터를operator new
함수에서 반환하는 것은 안전하다. - 그러나 지금의
operator new
함수에서는malloc
에서 나온 포인터를 반환하지 않는다. - 이 때문에 특정 경우에는 프로그램이 다운될 수 있다.
- 표준
- 언제 new 및 delete의 기본제공 버전을 다른 것으로 대체하는 작업을 하는 것이 의미가 있는지 다시 정리해보자.
잘못된 힙 사용을 탐지하기 위해
동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
할당 및 해제 속력을 높이기 위해
- 기본으로 제공되는 범용 할당자는 사용자 정의 버전보다 꽤 느린 경우가 적지 않다.
- 만들어야 할 응용프로그램이 단일 스레드로 동작하는데 컴파일러에서 기본으로 제공하는 메모리 관리 루틴이 다중 스레드에 맞게 만들어져 있다면, 스레드 안전성이 없는 할당자를 직접 만들어 상당한 속력 이득을 볼 수 있을 것이다.
- 물론 그 전에 적절한 프로파일링을 통해 프로그램 안에서 이들이 진짜로 병목을 일으키는 걸림돌인지 확인하는 것이 기본이다.
기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 범용 메모리 관리자는 메모리도 많이 잡아먹는 사례가 많다.
- 할당된 각각의 메모리 블록에 대해 전체적으로 지우는 부담이 꽤 되기 때문이다.
- 크기가 작은 객체에 대해 튜닝된 할당자를 사용하면 이러한 오버헤드를 실질적으로 제거할 수 있다.(부스트의 Pool 라이브러리에서 제공하는 할당자 등)
적당히 타협한 기본 할당자의 바이트 정렬 독작을 보장하기 위해
- x86 아키텍처에서는 double이 8바이트 단위로 정렬되어 있을 때 읽기 쓰기 속도가 가장 빠르다.
- 그러나 시중에 나와 있는 컴파일러 중에는 기본적으로 제공되는
operator new
함수가 double에 대한 동적 할당 시에 8바이트 정렬을 보장하지 않는 것들이 있다. - 이런 경우 기본제공
operator new
대신에 8바이트 정렬을 보장하는 사용자 정의 버전으로 바꿈으로써 프로그램 수행 성능을 끌어올릴 수 있다.
임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
- 한 프로그램에서 특정 자료구조 몇 개가 대개 한 번에 동시에 쓰이고 있다는 사실을 알고 있고, 이들에 대해서 페이지 폴트 발생 횟수를 최소화하고 싶을 경우, 해당 자료구조를 담을 별도의 힙을 생성함으로써 이들이 가능한 한 적은 페이지를 차지하도록 하면 좋은 효과를 볼 수 있다.
그때그때 원하는 동작을 수행하도록 하기 위해
- 종종 컴파일러가 주는 버전이 하지 못하는 일을
operator new
및operator delete
함수가 해주었으면 하고 바라는 때가 있다. - 메모리 할당과 해제를 공유 메모리에다 하고 싶은데 공유 메모리를 조작하는 일은 C API로밖에 할 수 없을 때가 그 예시이다.
- 이런 경우에 사용자 정의 버전을 만든다.(항목 52 참조)
- 또 다른 예시로 응용 프로그램 데이터의 보안 강화를 위해 해제한 메모리 블록에 0을 덮어 쓰는 사용자 정의
operator delete
를 만드는 경우도 생각해볼 수 있다.
항목 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자
- 기존 관례에 잘 맞는
operator new
를 구현하려면 다음의 요구사항만큼은 기본으로 지켜야 한다.- 반환 값이 제대로 되어 있어야 한다.
- 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다.(항목 49)
- 크기가 없는(0바이트) 메모리 요청에 대한 대비책을 갖춰야 한다.
- 실수로 ‘기본’ 형태의 new가 가려지지 않도록 해야 한다.
operator new
의 반환 값 부분은 지극히 간단하다. 요청된 메모리를 마련해 줄 수 있으면 그 메모리에 대한 포인터를 반환하는 것으로 끝이다. 마련해 줄 수 없는 경우에는bac_alloc
타입의 예외를 던지면 된다.- 구현은 조금 까다롭다.
operator new
함수는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출하는 식으로 메모리 할당을 2회 이상 시도하기 때문이다.- 즉, 어떻게든 어떤 메모리를 해제하는 데 실마리가 되는 동작을 new 처리자 함수 쪽에서 할 수 있을 것으로 가정하고 있다.
operator new
가 예외를 던지는 경우는 오직 new 처리자 함수에 대한 포인터가 널일 때이다.
operator new
는 0 바이트가 요구되었을 때조차도 적법한 포인터를 반환해야 한다.- 이러한 요구사항을 정리하여 비멤버 버전의
operator new
함수를 의사 코드로 작성하면 다음과 같다.
// 사용자 지정 new 함수는 다른 매개변수를 추가로 가질 수 있다.
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
// 0 바이트 요청이 들어오면, 이것을 1바이트 요청을 간주하고 처리
if(size == 0)
{
size = 1;
}
while(true)
{
// size 바이트를 할당해봄
if( /*할당이 성공한 경우*/ )
{
return ( /* 할당된 메모리에 대한 포인터 */ );
}
// 할당이 실패한 경우
// 현재의 new 처리자 함수가 어떤 것으로 설정되어 있는지 찾음
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)
{
(*globalHandler)();
}
else
{
throw std::bad_alloc();
}
}
}
- 외부에서 0 바이트를 요구했을 때 1바이트 요구인 것으로 간주하고 있다.
- new 처리자 함수의 포인터를 널(0)로 설정하고 바로 뒤에 원래의 처리자 함수로 되돌려 놓고 있다. 현재의 전역 new 처리자 함수를 얻어오는 직접적인 방법이 없기 때문이다.
- 단일 스레드라면 안전하게 동작할 것이고, 멀티 스레드 환경이라면 스레드 락을 걸어야 할 것이다.
operator new
함수에 무한 루프가 들어 있다는 이야기를 항목 49에서 했는데, 위의 코드를 보면 그 루프를 확인할 수 있다.- 종료의 조건이
while(true)
이기 때문에 루프를 빠져나오는 조건은 다음과 같을 수 밖에 없다.- 메모리 할당의 성공
- 항목 49에서 이야기한 동작들 중 한가지를 new 처리자 함수에서 실행
- 가용 메모리 확보
- 다른 new 처리자 설치
- new 처리자의 설치 제거
bad_alloc
혹은bad_alloc
의 파생 예외 던지기- 아예 함수 복귀를 포기하고 도중 중단(abort(), exit())
- new 처리자에서 루프를 벗어날 수 있는 동작을 정의하지 않았다면
operator new
의 내부 루프는 절대 스스로 끝나지 않는다.
- 종료의 조건이
operator new
멤버 함수는 파생 클래스쪽으로 상속이 되는 함수이다.- 특정 클래스 전용의 할당자를 만들어서 할당 효율을 최적화하기 위해서 사용자 정의 메모리 관리자를 작성할 수 있는데, 여기서
특정
클래스는 그 클래스 하나를 가리킬 뿐그 클래스 혹은 그 클래스로부터 파생된 다른 클래스들
모두를 지칭하는 것인 아니다. - 상속을 사용할 경우에는 파생 클래스 객체를 담을 메모리를 할당하는 데 기본 클래스의
operator new
가 호출되는 일이 발생할 수도 있다.
- 특정 클래스 전용의 할당자를 만들어서 할당 효율을 최적화하기 위해서 사용자 정의 메모리 관리자를 작성할 수 있는데, 여기서
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
// Derived에서는 operator new가 선언되지 않음
class Derived: public Base {
public:
};
// Base::operator new가 호출됨
Derived* p = new Derived;
- 이런 상황에서 Base 클래스의
operator new
가 적절한 조치를 취하도록 설계되지 않았다면 전체 설계를 바꾸지 않고 쓸 수 있는 가장 좋은 해결 방법은 틀린 메모리 크기가 들어왔을 때를 시작부분에서 확인한 후에 표준operator new
를 호출하는 쪽으로 살짝 비껴가게 만드는 것이다.
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
// 틀린 크기가 들어오면 표준 operator new 쪽에서 메모리 할당 요구를 처리함
if (size != sizeof(Base))
return ::operator new(size);
// 맞는 크기가 들어오면 나머지 작업을 여기에서 처리함
// ...
}
- 위의 코드에서는 0 바이트 요청에 대한 점검을 하는 코드가 존재하지 않는다.
- 하지만 실제로는 0 바이트 점검 코드는 실행이 된다.
sizeof(Base)
와size
를 비교하는 코드에 합쳐져 있다.- C++에는 모든 독립 구조의 객체는 반드시 크기가 0이 넘어야 하는 금기사항이 있다.
- 이런 정의 덕택에
sizeof(Base)
가 0이 될 일은 없으며, 이에 따라size
가 0이면if
문이 거짓이 되어 메모리 처리 요구가 표준operator new
쪽으로 넘어가게 된다.
- 배열에 대한 메모리 할당을 클래스 전용 방식으로 하고 싶다면,
operator new[]
함수를 구현하면 된다.operator new[]
안에서 할 일은 단순히 원시 메모리의 덩어리를 할당하는 것 밖에 없다.- 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해서 아무것도 할 수 없다.
- 사실, 배열 안에 몇 개의 객체가 들어갈지 계산하는 것조차도 안 된다.
- 객체 하나가 얼마나 큰지 확정할 방법이 없다. 파생 클래스 객체의 배열을 할당할 때 기본 클래스의
operator new[]
가 호출될 수도 있기 때문이다. 그리고 파생 클래스는 대체적으로 기본 클래스보다 더 크다. 따라서 객체 하나의 크기가sizeof(Base)
라는 가정을 할 수 없다. operator new[]
에 넘어가는size_t
타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정되어 있을 수도 있다. 동적으로 할당된 배열에는 배열 원소의 개수를 담기 위한 자투리 공간이 추가로 들어간다.
- 객체 하나가 얼마나 큰지 확정할 방법이 없다. 파생 클래스 객체의 배열을 할당할 때 기본 클래스의
operator delete
의 경우는 비교적 간단하다.- C++는 널 포인터에 대한 delete 적용이 항상 안전하도록 보장한다는 사실만 잊지 않으면 된다.
- 할 일은 이 보장을 유지하는 것 뿐이다.
- 다음은 비멤버 버전의
operator delete
의사 코드이다.
void operator delete(void* rawMemory) throw()
{
// 널 포인터가 delete 되려고 할 경우에는 아무것도 하지 않음
if(rawMemory == 0) return;
// rawMemory가 가리키는 메모리를 해제
}
operator delete
의 클래스 전용 버전도 단순하기는 매한가지이다.- 삭제될 메모리의 크기를 점검하는 코드를 추가적으로 넣어주기만 하면 된다.
- 클래스 전용의
operator delete
역시 틀린 크기로 할당된 메모리의 삭제 요청을 표준operator delete
로 전달하면 된다.
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory) throw();
}
void Base::operator delete(void* rawMemory) throw()
{
// 널 포인터 점검
if (rawMemory == 0) return;
if (size != sizeof(Base))
{
// 크기가 틀린 경우, 표준 operator delete로 전달
::operator delete(rawMemory);
return;
}
// rawMemory가 가리키는 메모리 해제
return;
}
- 마지막으로 가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는
operator delete
로 C++가 넘기는size_t
의 값이 엉터리일 수 있다.
메모
항목 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
- 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함된다.
항목 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자
- 관례적으로,
operator new
함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0 바이트에 대한 대책도 있어야 한다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 한다. operator delete
함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 한다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 한다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(381p ~ 392p) (0) | 2024.02.15 |
---|---|
이펙티브 C++(370p ~ 381p) (1) | 2024.02.14 |
이펙티브 C++(350p ~ 361p) (1) | 2024.02.12 |
이펙티브 C++(340p ~ 350p) (1) | 2024.02.10 |
이펙티브 C++(332p ~ 340p) (1) | 2024.02.09 |