readme.md
기록소
readme.md
전체 방문자
오늘
어제
  • 분류 전체보기
    • 네트워크
      • HTTP
      • 윈도우 소켓 프로그래밍
    • Windows API
    • 그래픽스
      • DirectX11
    • 일반
      • Linux
      • 데이터베이스
      • 팁
      • 책 후기
    • 쿠버네티스
    • 프로그래밍 언어
      • C#
      • Java
      • Go
      • C++
      • Lua
    • 책
      • 이펙티브 C++
      • 제프리 리처의 WINDOWS VIA C, C++
    • 기타

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 상속
  • CPP
  • C++
  • 자원관리
  • 캐스팅
  • windowsAPI
  • imagestride
  • 템플릿
  • const
  • 소멸자
  • DirectX
  • id3d11shaderresourceview
  • 윈도우 소켓
  • emplace
  • Delete
  • wm_keyup
  • 버텍스 버퍼
  • 인터페이스
  • 소켓 프로그래밍
  • 대입연산자
  • new
  • consteval
  • phong
  • directx11
  • 초기화
  • 생성자
  • 설계
  • 자바8
  • Graphics
  • 가상함수

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
readme.md

기록소

책/이펙티브 C++

이펙티브 C++(360p ~ 369p)

2024. 2. 13. 20:52

이펙티브 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를 구현하려면 다음의 요구사항만큼은 기본으로 지켜야 한다.
    1. 반환 값이 제대로 되어 있어야 한다.
    2. 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다.(항목 49)
    3. 크기가 없는(0바이트) 메모리 요청에 대한 대비책을 갖춰야 한다.
    4. 실수로 ‘기본’ 형태의 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[] 안에서 할 일은 단순히 원시 메모리의 덩어리를 할당하는 것 밖에 없다.
    • 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해서 아무것도 할 수 없다.
    • 사실, 배열 안에 몇 개의 객체가 들어갈지 계산하는 것조차도 안 된다.
      1. 객체 하나가 얼마나 큰지 확정할 방법이 없다. 파생 클래스 객체의 배열을 할당할 때 기본 클래스의 operator new[]가 호출될 수도 있기 때문이다. 그리고 파생 클래스는 대체적으로 기본 클래스보다 더 크다. 따라서 객체 하나의 크기가 sizeof(Base)라는 가정을 할 수 없다.
      2. 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
    '책/이펙티브 C++' 카테고리의 다른 글
    • 이펙티브 C++(381p ~ 392p)
    • 이펙티브 C++(370p ~ 381p)
    • 이펙티브 C++(350p ~ 361p)
    • 이펙티브 C++(340p ~ 350p)
    readme.md
    readme.md

    티스토리툴바