(Win32 : WindowsProgramming-24) Heap Memory

Posted by : at

Category : win32   WindowsProgramming


#include <stdio.h>
#include <Windows.h>

int main()
{
    void* p = VirtualAlloc(
                0,      // 메모리 주소(0을 넘기면 OS가 자동할당, 내가 할당하고 싶다면 64k의 배수로 넘겨야한다)
                100,    // 메모리 크기(PAGE의 배수로 할당되기에 4096)
                MEM_RESERVE | MEM_COMMIT,
                PAGE_READWRITE);

    VirtualFree(p, 0, MEM_RELEASE);
}

만약 진짜로 100Mb만 필요했다면??
일단은 4096Mb를 할당후 할당된 4096안에서 나눠서 쓴다
우선 100을 쓰고 나중에 또 필요하면 거기서 또 쓰고 이런식

이런 방법이 Windows API 자체적으로 구현이 되어있다.


Heap은 기본적으로 프로세스가 생성될 시 1Mb를 할당해 주는데 이 기본 Heap을 사용할 경우 -> GetProcessHeap() -> HeapAlloc() -> HeapFree() 의 절차
사용자가 추가로 Heap을 만드는 경우 -> 뒤에서 알려줌.

  • 힙 할당시 플래그는
    • HEAP_ZERO_MEMORY : 할당된 메모리 블록을 0으로 초기화
    • HEAP_GENERATE_EXCEPTIONS : 할당실패시 예외 발생해 달라
    • HEAP_NO_SERIALIZE : 동기화를 수행하지마라(이후에 설명)
#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE heap = GetProcessHeap();
    int* p1 = (int*)HeapAlloc(heap,
                            HEAP_ZERO_MEMORY    // 0으로 메모리 초기화
                            sizeof(int) * 10);

    printf("%p\n", p1);

    p1[0] = 10;
    p1[1] = 20;

    HeapFree(heap, 0, p1);
}
  • 프로세스는 생성되면 기본적으로 1M의 기본 힙이 할당된다
    • 기본 힙의 핸들은 GetProcessHeap()로 얻을수 있다
    • HeapAlloc()을 통해 힙에 메모리할당가능, 여기서 힙 핸들을 넘겨 기본핸들에 할당할지 자신이 할당한 힙에 할당할지 결정
  • 큰 블록(1M이상)의 메모리의 경우
    • VirtualAlloc()을 사용하는 것을 권장
  • 작은 크기의 메모리 블럭
    • Heap을 사용하는 것을 권장
  • malloc(new) Vs HeapAlloc의 차이점?
    • 뒤에서 설명

힙의 크기확인/확장

#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE heap = GetProcessHeap();
    int* p1 = (int*)HeapAlloc(heap,
                            HEAP_ZERO_MEMORY
                            40);

    int sz = HeapSize(heap, 0, p1); // 할당한 사이즈를 확인 : 40
    
    HeapReAlloc(heap, 0, p1, 30);   // 40 -> 30
    int* p2 = HeapReAlloc(heap, 0, p1, 60);   // 30 -> 60
    // 리턴으로 메모리 주소를 받는 것은 메모리 공간을 이전하여 확장할 수 있기 때문이다.
    // 이전 확장이 필요한 이유는 p1의 메모리공간이 충분하지 않을경우 p1에 할당이 불가능하기 때문이다.

    HeapFree(heap, 0, p1);
}
  • HeapReAlloc의 플래그도 기본적으로 힙 할당플래그와 동일하고 마지막 하나만 다르다
    • HEAP_ZERO_MEMORY : 할당된 메모리 블록을 0으로 초기화
    • HEAP_GENERATE_EXCEPTIONS : 할당실패시 예외 발생해 달라
    • HEAP_NO_SERIALIZE : 동기화를 수행하지마라
    • HEAP_REALLOC_IN_PLACE_ONLY : 메모리를 이동하지 말고 크기 변경, 불가능할 경우 실패

기본 힙, 페이지를 변경할 순 없나?

물론가능

$ cl heap.cpp /link /HEAP:1048576,4096
#/HEAP:[기본힙사이즈],[페이지]

VS에서는 속성 -> 링커 -> 시스템 -> 힙 예약 크기, 힙 커밋 크기를 변경하면된다.

단, 일반적으론 힙의 크기는 자동증가하기에 보통은 디폴트 값으로 두고 쓴다


추가 힙 생성

  • 힙의 생성화 해제는 : HeapCreate() -> HeapDestroy() 로 가능하다
  • 그런데 왜 기본 힙을 사용하지 않을까?

힙을 하나만 사용했을때 단점을 우선 알아야 한다.

Linked list 인 Node 1, 2를 Heap 에 할당했다 가정해보자.
(말이 좀 헷갈리는데 Linked List 2개(Node 1, Node 2)를 힙에 할당했다는 말
Heap 에는 아래와 같이 할당 됐다고 가정하자

<< Heap 1 >>
| Node1 | Node 2 | Node 1 | Node 2 |

문제점

  • Node1에서 메모리접근 실수로 Node2에 문제를 발생시킬수 있음(메모리가 너무 딱 붙어있음.)
  • Node1의 메모리를 해지해도 메모리의 이득이 없다(메모리 단편화(fragment) 현상)
    • Node1이 지워진다고 가정하면 애매하게 Node1의 메모리공간이 단편화 된다.
<< Heap 1 >>
| Node 1 | Node 1 | 

<< Heap 2 >>
| Node 2 | Node 2 |
  • 자료구조별로 다른 힙을 사용하면
    • 안정성이 좋고, 메모리 단편화 현상이 해결된다.
    • 메모리 해지시 Heap을 해지해버리면 된다.(빠르다)
    • 연속된 주소공간에 할당되기에 캐쉬적중률이 높다
    • 기본Heap의 경우 다양한 API함수가 내부적으로 메모리가 필요할때 사용되기에 사용이 잦다
    • HeapAlloc() Serialize(동기화, thread lock을 건다는 말)되는데 다양한 Thread에서 접근가능성이 있기때문이다. 이부분도 성능에 문제를 발생시킬 수 있다.
    • 내가 만든 Heap은 Serialize를 안시킬수 있는 옵션이 별도로 있어 성능향상을 볼 수 있다.
#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE heap1 = GetProcessHeap();
    HANDLE heap2 = HeapCreate(HEAP_NO_SERIALIZE,    // 동기화는 하지마라
                                4096,               // 확정크기
                                0);                 // 최대 크기(0 : 무제한)

    void* p1 = HeapAlloc(heap1, HEAP_ZERO_MEMORY, 100);
    void* p2 = HeapAlloc(heap2, HEAP_ZERO_MEMORY, 100);

    printf("%p\n", p1);
    printf("%p\n", p2);

    HeapFree(heap1, 0, p1);
    HeapFree(heap2, 0, p2);

    // 기본힙은 프로세스 종료시에 자동으로 HeapDestroy호출됨.
    HeapDestroy(heap2);
}

생성된 힙 순회하기

#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE heap1 = GetProcessHeap();
    HANDLE heap2 = HeapCreate(0, 4096, 0);

    HANDLE handle[256];
    // 모든 힙을 열거
    int cnt = GetProcessHeaps(256, handle);


    for(int i = 0; i < cnt; i++)
    {
        printf("%d : %x\n", i, handle[i]);
    }

    HeapDestroy(heap2);
}

CRT vs API

  • 우선 좀 정리하자면,
    • VirtualAlloc()를 통해서 PAGE단위로 Physical Storage를 할당한다
    • PAGE단위로 할당되기에 HeapAlloc()로 조금씩 나눠서 쓴다
  • malloc / new 도 결국 HeapAlloc() or VirtualAlloc()을 호출하게 된다.

  • 그럼 언제 쓰나?
    • Windows OS만을 위해 만들어진 프로그램이다 -> HeapAlloc()쓰는 것을 추천
    • Linux등 다른 OS도 지원해야 한다 -> malloc / new 사용
      • 참고로 최신VS(2019~)에서는 malloc할당을 할경우 기본힙을 사용함.
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

int main()
{
    HANDLE h1 = GetProcessHeap();

    void* p1 = HeapAlloc(h1, 0, 100)
    void* p2 = malloc(100);
    void* p3 = new char[100];

    // 세 주소다 다 비슷한곳에 할당됨(결국 모두 HeapAlloc() 쓴다는 말)
    printf("HeapAlloc : %p\n", p1);
    printf("malloc : %p\n", p2);
    printf("new : %p\n", p3);

    delete[] p3;
    free(p2);
    HeapFree(h1, 0, p1);
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links