일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 반복자
- Vector
- exception
- 암시적 변환
- 언리얼
- resource management class
- 티스토리챌린지
- 다형성
- 영화
- implicit conversion
- 스마트 포인터
- 오블완
- Smart Pointer
- 상속
- 메타테이블
- 비교 함수 객체
- 영화 리뷰
- 루아
- 참조자
- operator new
- 게임
- UE4
- c++
- reference
- 예외
- more effective c++
- Effective c++
- effective stl
- lua
- virtual function
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 50. operator new / delete는 언제 커스텀해야 할까? 본문
728x90
항목 50 : new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해두자
operator new / delete를 커스텀하는 이유
- 잘못된 힙 사용 탐지를 위해
- new 한 메모리에 대해 delete를 잊으면 메모리 누수가 발생한다.
- new 한 메모리에 두번 이상 delete 하면 미정의 동작을 한다.
- 할당된 메모리 주소의 목록을 operator new가 유지하고 delete에서 제거하게 만들면 실수를 방지할 수 있을 것이다.
- 데이터 오버런(overrun, 할당된 메모리 블록 끝을 넘어 뒤에 기록), 데이터 언더런(underrun, 할당된 메모리 블록 시작을 넘어 앞에 기록)이 발생할 수 있다.
- 메모리를 약간 더 할당해서 오버런 / 언더런 탐지용 바이트 패턴을 구현할 수 있다.
- 효율을 향상시키기 위해
- 컴파일러가 기본적으로 제공하는 operator new / delete 함수는 지극히 대중적이고 온건지향 스타일의 전략을 취한다.
- 실행 기간이 짧지 않은 프로그램(웹서버)에서 잘 돌아가야 한다.
- 1초 안에 끝나는 프로그램에서도 문제가 없어야 한다.
- 큰 블록만 할당하든, 작은 블록만 할당하든, 크고 작은 블록을 섞어서 할당하든 무난하게 처리해야 한다.
- 수명이 매우 길거나 짧은 객체를 아주 많이 할당했다가, 해제했다가 하는 작업까지도 소화해야 한다.
- 힙 단편화(fragmentation) 대처 방안도 필요하다.
- 그러므로 개발자가 자신의 프로그램이 동적 메모리를 어떻게 사용하는지 제대로 이해한다면, 만들어 쓰는 편이 우수한 성능을 낼 확률이 높다.
- 컴파일러가 기본적으로 제공하는 operator new / delete 함수는 지극히 대중적이고 온건지향 스타일의 전략을 취한다.
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 커스텀 new / delete를 무작정 작성하기 전에 정보를 수집하는 것이 좋을 것이다.
- 모으면 좋은 정보들
- 할당된 메모리 블록의 크기 분포
- 메모리 블록의 사용 기간 분포
- 메모리가 할당, 해제되는 순서가 FIFO(선입선출)인지, LIFO(후입선출)인지, 순서가 없는지
- 시간 경과에 따라 메모리 사용 패턴이 바뀌는지
- 즉, 각 실행 단계마다 프로그램이 보이는 메모리 할당/해제 패턴이 확연한 차이를 보이는지
- 한번에 실제로 쓰이는 동적 할당 메모리의 최대량(최고수위선(high water mark))은 어떤지
예제를 통해 알아보는 커스텀 new / delete로 정보 수집하기
- 버퍼 오버런, 언더런을 탐지하는 커스텀 new / delete
static const int signature = 0xDEADBEEF;
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);
// 메모리를 얻어낸다.
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 참조)
- 대개 operator new 함수를 만들 때 통상적으로 쓰이는 관례를 지키지 않은 점이 문제이다.
- 가장 큰 문제점은, 바이트 정렬(alignment)
바이트 정렬(byte alignment) 문제
- 컴퓨터는 통상적으로 아키텍처(architecture)적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다.
- 포인터는 4의 배수에 해당하는 주소에 맞추어 저장되어야 한다.(즉, 4바이트 단위로 정렬되어야 한다.)
- double 값은 8의 배수에 해당하는 주소에 맞추어 저장되어야 한다.(즉, 8바이트 단위로 정렬되어야 한다.)
- 어떤 아키텍처의 경우 이 바이트 정렬 제약을 따르지 않으면 프로그램이 하드웨어 예외를 일으킬 수도 있다.
- 또 어떤 아키텍처는 느슨한 제약일지라도, 바이트 정렬을 만족했을 때 더 나은 성능을 보일 때가 있다.
- 인텔 x86 아키텍처
- 위의 코드 역시 바이트 정렬에서 안전하지 않다는 문제점이 있다.
- 모든 operator new 함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다는 것이 C++의 요구사항이기 때문이다.
- 표준 malloc 함수는 이 요구사항에 맞추어 구현되어 있다.
- 하지만 위의 커스텀 operator new는 malloc 함수가 준 포인터를 기준으로 int 크기만큼 뒤로 어긋난 주소를 포인터로 반환한다.(안전하지 않다!)
- 사실 이런 식으로 직접 만들 필요조차 없다.
- 시중의 컴파일러 중엔 메모리 관리 함수에 디버깅 및 로딩 기능을 넣어 놓고 필요에 따라 전환할 수 있도록 마련된 것들도 있다.
- 메모리 관리 함수만을 전문적으로 다루는 상업용 제품들도 출시되어 있다.
- 오픈 소스 쪽에도 메모리 관리자 패키지가 많이 공개되어 있다.
- 부스트의 풀(Pool) 라이브러리가 대표적이다.(항목 55 참조)
다시 한 번 정리하는 커스텀 operator new / delete가 필요한 상황
- 잘못된 힙 사용을 탐지하기 위해
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속력을 높이기 위해
- 기본 제공 범용 할당자는 사용자 정의 버전보다 꽤 느린 경우가 적지 않다.
- 특히 사용자 정의 버전이 특정 타입의 객체에 맞추어 설계되어 있다면 더욱 그렇다.
- 부스트 Pool 라이브러리에서 제공하는 할당자처럼 고정된 크기의 객체만 만들어 주는 할당자의 전형적인 응용 예가 바로 클래스 전용(class-specific) 할당자이다.
- 기본 제공 메모리 관리 루틴이 다중 쓰레드에 맞춰져 있다면?
- 스레드 안정성이 없는 할당자를 직접 만들 수도 있을 것이다.
- 기본 제공 범용 할당자는 사용자 정의 버전보다 꽤 느린 경우가 적지 않다.
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 범용 메모리 관리자는 사용자 정의 버전과 비교해서 느리고 메모리도 많이 잡아먹는 사례가 허다하다.
- 할당된 각각의 메모리 블록을 전체적으로 지우는 부담이 꽤 되기 때문이다.
- 크기가 작은 객체에 대해 튜닝된 할당자를 사용하면 오버헤드를 실질적으로 제거할 수 있다.
- 범용 메모리 관리자는 사용자 정의 버전과 비교해서 느리고 메모리도 많이 잡아먹는 사례가 허다하다.
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
- x86 아키텍처는 double이 8바이트 단위로 정렬되어 있을 때, 읽기 쓰기 속도가 가장 빠르다.
- 시중에 나와 있는 컴파일러 중에는 기본적으로 제공하는 operator new 함수가 double에 대한 동적 할당 시에 8바이트 정렬을 보장하지 않는 것들이 있다는 슬픈 소식이 나돌고 있다...
- 이 경우 8바이트 정렬을 보장하는 커스텀 operator new를 만들 수도 있을 것이다.
- x86 아키텍처는 double이 8바이트 단위로 정렬되어 있을 때, 읽기 쓰기 속도가 가장 빠르다.
- 임의의 관계를 맺고 있는 객체들을 한군데에 나란히 모아놓기 위해
- 한 프로그램에서 특정 자료구조 몇 개가 대개 한 번에 동시에 쓰이고 있다는 사실을 이미 알고 있고..
- 이들에 대해 페이지 부재(page fault) 발생 횟수를 최소화하고 싶을 경우
- 해당 자료구조를 담을 별도의 힙을 생성해서 가능한 한 적은 페이지를 차지하도록 하면 효과를 볼 수 있다.
- 메모리 군집화는 위치지정(placement) new / delete로 쉽게 구현이 가능하다. (항목 52 참조)
- 한 프로그램에서 특정 자료구조 몇 개가 대개 한 번에 동시에 쓰이고 있다는 사실을 이미 알고 있고..
- 그때 그때 원하는 동작을 수행하도록 하기 위해
- 컴파일러가 주는 버전이 하지 못하는 일을 operator new / delete가 해주었으면 할 때가 있다.
- 예시 1) 메모리 할당 해제를 공유 메모리에다가 하고 싶은데 공유 메모리를 조작하는 건 C API로밖에 할 수 없을 때
- 위치지정(placement) new / delete로 처리할 수 있다.(항목 52 참조)
- 예시 2) 응용 프로그램 데이터의 보안 강화를 위해, 해제한 메모리 블럭에 0을 덮어쓰는 사용자 정의 operator delete를 만드는 경우
- 예시 1) 메모리 할당 해제를 공유 메모리에다가 하고 싶은데 공유 메모리를 조작하는 건 C API로밖에 할 수 없을 때
- 컴파일러가 주는 버전이 하지 못하는 일을 operator new / delete가 해주었으면 할 때가 있다.
728x90
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 52. 위치지정 new / delete (0) | 2024.07.26 |
---|---|
[Effective C++] 51. operator new / delete 커스텀 관례 (0) | 2024.07.25 |
[Effective C++] 49. new 처리자 (6) | 2024.07.23 |
[Effective C++] 48. 템플릿 메타 프로그래밍 (0) | 2024.07.22 |
[Effective C++] 47. 특성 정보 클래스 (0) | 2024.07.19 |
Comments