Corgi Dog Bark

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [c++] delete[] 가 포인터를 받아도 잘 동작하는 이유(over-allocate)
    뜯고 또 뜯어보는 컴퓨터/씨쁠쁠 C++ 2022. 11. 9. 16:23
    반응형

    c++에서는 동적으로 객체를 생성 및 삭제를 시켜줄 때, new 그리고 delete 키워드를 쓰게 됩니다. 이때, new는 우리가 메모리를 할당하고, 생성자를 호출하게 되어, 그 사이즈나 크기를 명시해주게 됩니다.

     

    그렇지만 delete는 포인터만 넘겨줄 뿐인데, 어떻게 그 사이즈를 예측하고 삭제시켜 줄 수 있을까? 

     

    부연하자면, 밑과 같은 코드가 존재했을때, pFoo는 단순 포인터일 뿐인데, 어떻게 Foo array에 해당하는 메모리를 삭제시키고 알맞는 개수의 소멸자를 호출할 수 있을까요?

    class Foo {
    public:
        Foo() {}
        ~Foo() {}
        int mNunber;
    }
    
    int main() {
        Foo* pFoo = new Foo[10];
        
        delete[] pFoo;
    }

     

     

     

    메모리 할당에 해당 2가지 방법론


    이를 위해서 컴파일러는 2가지 방법론을 주로 사용하고 있습니다. 바로

    1. "over-allocation" 
    2. "associative array"

    라는 2가지 방법론을 구사하고 있습니다. 2가지중 어떤 것을 사용하는지 컴파일러 구현 방법에 따라 다르니 확인 바랍니다.

     

    먼저 Over-Allocation에 대해서 설명해보도록 하겠습니다.

     

     

     

     

    Over-Allocation - new


    컴파일러가 Over-Allocation 기법을 사용하기로 했다면, p = new Foo [N]을 호출했을 때는 다음과 같은 테크닉을 쓰게 됩니다. 밑에서 설명하는 WORDSIZE의 경우, 컴파일러에 의존적인 상수인데요, 보통 sizeof(size_t)를 나타내게 됩니다. 32비트에서는 4, 64비트에서는 주로 8이 되는 것을 확인할 수 있습니다. (컴파일러 설계에 따라 다릅니다.)

     

    그럼 코드 살펴보겠습니다. 우선 new에 해당하는 코드를 살펴보겠습니다.

    // Original code: Foo* pFoo = new Foo[n];
    
    char* tmp = (char*) operator new[] (WORDSIZE + n * sizeof(Foo)); // WORDSIZE 에 해당하는 메모리 할당
    
    Foo* pFoo = (Foo*) (tmp + WORDSIZE); // WORDSIZE 만큼 이동후, Foo* p 라는 포인터 할당
    
    *(size_t*)tmp = n;	// tmp 에 n 을 대입, 즉 몇개의 객체가 있는지 확인
    
    for (size_t i = 0; i < n; ++i)	// pFoo + i 를 돌면서 객체 생성자 호출
      new(pFoo + i) Foo();
    1. tmp에 WORDSIZE + 객체 배열의 사이즈에 해당하는 메모리 할당을 하고,
    2. WORDSIZE 만큼 이동후, Foo* p라는 포인터에 실제 배열 포인터를  할당합니다.
    3.  tmp에 n을 대입하여 즉 몇 개의 객체가 있는지 확인할 수 있도록 합니다.
    4. 마지막으로 p + i를 돌면서 foo 객체 생성자 호출하게 됩니다.

    다음과 같은 과정을 띄게 되는데, Over-Allocation의 경우, 배열 앞에 약간의 특정 메모리 공간을 할당하여, 배열의 객체가 몇 개 있는지 확인할 수 있도록 합니다.

     

    반응형

     

    Over-Allocation - delete


    그럼 이제 delete[] operator를 적용하게 되면, 어떤 식으로 작동되는지 확인해보겠습니다. delete[] 연산자를 적용하게 되면, 다음과 같이 컴파일러는 해석하게 됩니다.

    // Original code: delete[] pFoo;
    
    size_t n = * (size_t*) ((char*)pFoo - WORDSIZE);	// WORDSIZE 만큼 메모리 이동후, 사이즈 확인
    
    while (n-- != 0)			// N 만큼의 Foo 소멸자 호출
      (pFoo + n)->~Foo();
      
    operator delete[] ((char*)pFoo - WORDSIZE);	// pFoo에서 WORDSIZE 만큼 뺀후, 메모리 해제

    아까 new에서 over-allocation에 할당한 수를 통해, array의 개수를 파악할 수 있으며, 이를 통해 객체의 개수만큼 소멸자를 호출 후, 메모리를 해제해 주는 것을 볼 수 있습니다.

     

     

    그럼 실제 코드를 컴파일 한 경과를 통해, 어셈블리를 분석한 것을 나타내고 보여드리겠습니다.

    #include <iostream>
    
    const int n = 10;
    
    struct Foo {
    public:
        int b_var;
        ~Foo(){}
    };
    
    int main() {
        Foo* bFoo = new Foo[n];
        delete[] bFoo;
    }

    위 코드를 어셈블리로 변환하면,

    다음과 같이 Foo의 사이즈 * 10 해서, 40 에다가 + 8 만큼의 메모리를 더 할당하는 것을 볼 수 있습니다.

     

     

     

    Over-Allocation - 정리


    정리하자면, over-allocation의 경우, 배열의 메모리 할당 여부를 객체 앞에 위치시켜 얼마만큼의 메모리가 할당했는지 알 수 있도록 하는 테크닉이라고 정리할 수 있을 것입니다. 다음 화에서 설명드릴 "associative array technique"과 비교해서, over-allocation 테크닉은 속도가 빠르다고 할 수 있겠지만, 프로그래머가 delete [] 대신 delete를 사용했을 때, 오류가 발생할 수 있습니다. 

     

     

     

    긴 글 읽어주셔서 감사합니다.

    도움이 되셨거나, 잘못된 내용이 있다면 댓글 부탁드리겠습니다.

    반응형

    댓글

Designed by Tistory.