힙마지막으로 힙에 대해 알아봅시다. 힙은 작은 블럭의 많은 데이터를 관리할 때 가장 유용합니다. 힙이 사용되는 가장 대표적인 예는 C/C++에서 malloc, new 등을 이용한 동적 메모리 할당입니다. 내부적으로 힙은 확보된 주소 공간 영역입니다. 다시 말해 기본적으로는 가상 메모리를 이용해서 구현된 높은 수준의 메모리 관리 메커니즘입니다. 사용자가 더 많은 메모리를 힙에서 할당받을수록 힙 관리자는 더 많은 메모리를 커밋하게 됩니다.
언제 힙을 사용하는가?프로세스가 생성될 때 시스템은 프로세스의 주소 공간에 하나?힙을 생성합니다. 이 힙을 기본 힙(default heap)이라고 부릅니다. 기본 값으로 이 힙은 1MB의 크기를 가집니다. 하지만 하나의 프로세스는 기본 힙 이외에 여러 개의 힙을 가질 수 있습니다. 여러 개의 힙을 사용하는 경우에 대해 Jeffrey Richter의 「Programming Applications for Microsoft Windows 4th Ed.」 에는 다음과 같이 5가지의 용도에 대해 설명하고 있습니다.
◆ 컴포넌트 보호
만일 서로 다른 두 개 타입의 구조체가 있다고 합시다. 두 구조체는 서로 크기가 다르기 때문에 하나의 힙에 이 두 구조체 타입의 인스턴스를 관리한다면 프로그래머가 실수로 힙을 깨뜨릴 수도 있습니다. 이런 것을 방지하기 위해 아예 다른 힙을 생성해 각각의 구조체를 전담시킨다면 이런 위험이 많이 줄어들 것입니다.
◆ 효율적인 메모리 관리
힙은 같은 크기의 메모리를 할당할 때 가장 효율적으로 관리됩니다. 따라서 크기가 다른 두 개의 구조체가 있을 때 이들 각각을 서로 다른 힙으로 관리한다면 더 효율적인 메모리 관리가 가능합니다.
◆ 로컬 액세스
페이지 폴트는 시스템의 성능에 많은 문제를 일으킵니다. 따라서 만일 데이터의 접근을 한정된 영역에 집중시키면 시킬수록 프로그램의 성능은 올라갑니다. 서로 다른 두 개의 구조체가 있을 때 이들을 하나의 힙에서 관리한다면 각 구조체의 인스턴스들이 혼재하게 되어 데이터의 집중도가 떨어질 것입니다. 각각의 구조체를 서로 다른 힙으로 관리한다면 각 구조체 타입별로 인스턴스들이 인접하게 되어 성능이 향상될 수 있습니다.
◆ 쓰레드 동기화 부하 해결
기본적으로 힙은 순서화(serialization)를 보장합니다. 이는 여러 쓰레드들이 문제없이 힙에서 동시에 메모리 블럭을 할당하고 해제할 수 있게 해 줍니다. 하지만 이를 위해 당연히 추가적인 코드가 실행되게 됩니다. 만일 각각의 쓰레드가 각자의 힙을 따로 가지고 이런 순서화를 적용하지 않는다면 성능상의 이점이 있을 수 있습니다.
◆ 빠른 해제
앞에서와 같이 각각의 구조체마다 전담의 힙을 두게 된다면 해당 구조체의 전체 메모리 인스턴스를 해제할 때 각각 해제하는 것이 아니라 힙 전체를 한번에 해제할 수 있어 성능상의 이점이 있을 수 있습니다.
추가적인 힙의 사용추가적인 힙은 먼저 어떤 쓰레드에서 HeapCreate 함수를 부르는 것으로 가능합니다.
HANDLE HeapCreate(
DWORD flOptions,
SIZE_T dwInitialSize,
SIZE_T dwMaximumSize
);
flOptions 인자는 0, HEAP_NO_SERIALIZE, HEAP_ GENERATE_EXCEPTIONS 셋 중의 하나의 값이나 이들을 비트 OR 연산한 값을 가집니다. 여기서 주목할 만한 것은 HEAP_NO_SERIALIZE 값인데 이는 새로 생성하는 힙에서 순서화를 사용하지 않겠다는 의미입니다. 다시 말해 새로운 메모리 블럭을 여러 쓰레드에서 생성하려 한다면 힙은 매우 높은 확률로 깨질 수도 있습니다. 따라서 반드시 다음의 조건들 중 하나 이상이 참일 때만 HEAP_NO_SERIALIZE 값을 사용해야 합니다.
◆ 프로세스에 쓰레드는 오직 하나일 경우
◆ 다중 쓰레드가 있더라도 해당 힙에는 오직 하나의 쓰레드만 접근하는 경우
◆ 항상 크리티컬섹션, 뮤텍스, 세마포어 등을 이용한 쓰레드 동기화 메커니즘을 이용해 힙에 접근할 경우
dwInitialSize는 처음 힙을 생성할 때 커밋할 힙의 크기를 말하며 인자로 준 값은 페이지 크기로 올림하여 처리됩니다. dwMaximumSize는 생성하려는 힙의 최대 크기로 만일 0으로 주면 계속해서 크기가 증가하는 힙이 되고 0 이상의 값을 주면 힙은 한계 크기를 가지게 됩니다. 이 때 주의해야 할 점은 내부적으로 힙을 관리하기 위해 힙 자체에 사용되는 메모리가 있기 때문에 정확히 맞추어 힙의 크기를 할당하면 나중에 원하는 만큼의 메모리를 할당 받을 수 없을 수도 있다는 것입니다. 따라서 힙을 생성할 때에 최대 크기를 정한다면 넉넉하게 정해 주어야 합니다. 일단 힙을 생성했으면 힙에서 어떤 메모리 블럭을 할당받을 수 있습니다. 이 때 사용되는 함수가 HeapAlloc입니다.
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
SIZE_T dwBytes
);
첫 인자인 hHeap은 앞에서 생성한 힙의 핸들을 받습니다. dwBytes는 할당받을 메모리의 크기이며 dwFlags는 HEAP _ZERO_MEMORY, HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE의 세 가지 값이나 HEAP_ZERO_ MEMORY를 주면 할당받은 메모리를 모두 0으로 초기화하며 HEAP_NO_SERIALIZE는 앞에서와 유사하게 HeapAlloc을 순서화를 위한 코드를 실행하지 않고 수행하게 됩니다. HEAP_GENERATE_EXCEPTIONS 값을 주면 할당에 실패했을 때 예외를 발생시킵니다. 98 계열에서는 256MB 이상의 HeapAlloc 요구는 에러로 간주되며 HeapAlloc은 실패하고 그냥 NULL을 리턴합니다. 이 때 HEAP_GENERATE_ EXCEPTIONS 값을 주었더라도 예외는 발생하지 않는다는 것을 98 계열에서는 반드시 주의하기 바랍니다. 만일 할당하고자 하는 메모리가 1MB를 넘는다면 VirtualAlloc을 사용하기 바랍니다. 큰 크기의 메모리는 힙을 이용하는 것이 바람직하지 않습니다.
어떤 힙의 메모리 블럭을 다 사용했다면 HeapFree 함수를 통해 이를 해제하고 힙의 사용이 끝났다면 HeapDestroy 함수를 통해 힙을 파괴해야 합니다. 단, 프로세스의 디폴트 힙은 해당 프로세스가 끝날 때까지 파괴되지 않습니다.
메모리 관리는 프로그램 성능과 직결이번에는 윈도우에서 제공하는 메모리 관리 메커니즘인 가상 메모리, 메모리 맵 파일, 힙에 대해 알아봤습니다. 지면 관계상 다소 간략하게 소개된 면이 있는 것아 아쉬운 감이 있습니다. 메모리 관리는 프로그램의 성능에 매우 지대한 영향을 미치는 중요한 요소이기 때문에 MSDN이나 참고자료를 통해 가능한 자세히 파고 들 것을 권합니다. 이제 이 연재도 다음이 마지막이군요. 마지막에는 윈도우의 모든 것이라 해도 과언이 아닐 DLL에 대해 알아보도록 하겠습니다