Memory & object pooling

13
Memory & Object Pooling NHN NEXT 남현욱

Transcript of Memory & object pooling

Page 1: Memory & object pooling

Memory & Object Pooling

NHN NEXT남현욱

Page 2: Memory & object pooling

Windows LFH잦은 메모리 할당 / 해제의 반복시 힙에 남은 메모리의 양이 충분함에도 불구하고 더 이상 메모리의

할당을 진행할 수 없는 상황이 생긴다. 이를 Heap fragmentation이라고 하는데, LFH는 이 현상

을 줄이기 위해 고안된 메모리 할당 정책이다.

Heap - 빨강 : 할당됨, 흰색 : 할당 안 됨

이 정도 크기 할당하려고 할 때 힙에 여유 공간이 충분히 남아 있음에도 불구하고 부분부분 잘려있어 할당이 불가능하다!! -> fragmentation

Page 3: Memory & object pooling

Windows LFHLFH는 이를 해결하기 위해 128개의 서로 다른 크기의 bucket을 미리 구성한 후, 메모리 할당 요청

이 일어났을 때 해당 사이즈를 포함할 수 있는 가장 작은 크기의 bucket을 선택해 거기 할당한다.

Buckets Granularity(증가폭) Range(사이즈 범위)

1 - 32 8 1-256

33 - 48 16 257-512

49 - 64 32 513-1024

65 - 80 64 1025-2048

81 - 96 128 2049-4096

97 - 112 256 4097-8192

113 - 128 512 8193-16384

따라서 버킷 사이즈는 차례대로 1번 = 8Bytes, 2번 = 16Bytes, 3번 = 24Bytes, 4번 = 32Bytes...

크기가 된다. 만약 21Byte 크기의 메모리 할당 요청이 들어온다면 해당 크기를 수용할 수 있는 가장

작은 크기의 버킷은 24Bytes 사이즈의 3번 버킷이므로 3번 버킷이 메모리 할당에 쓰인다.

Page 4: Memory & object pooling

Windows LFHLFH를 쓰기 위한 전제 조건이 있다(아래 조건을 만족하지 않으면 LFH를 사용할 수 없다).

• 할당 크기는 16kb보다 작아야한다. 앞의 표에서도 볼 수 있지만 16kb보다 큰 크기의 bucket은

없기 때문에 어찌보면 당연하다.

• HEAP_NO_SERIALIZE(꼭 순차적으로 접근하지 않아도 되며 접근시 lock이 걸리지 않는 heap)

로 생성된 heap에는 사용이 불가능하다.

• 고정 크기(Fixed Size)로 생성된 heap에는 적용이 되지 않는다.

• 디버깅 툴에서는 LFH가 동작하지 않는다.

그리고 한 번 LFH를 적용시키면 이걸 되돌리는 것은 불가능하다.

LFH는 16KB 미만의 메모리 할당이 빈번하게 일어나는 멀티 스레드 환경에서 뛰어난 성능을 보인

다고 한다.

Page 5: Memory & object pooling

Windows LFH사용법. 매우 간단하다. windows Vista 이상부터는 기본적으로 LFH를 사용한다고 한다.

ULONG heapFragValue = 2;

HeapSetInformation(GetProcessHeap(), //default heap의 핸들을 받아옴

HeapCompatibilityInformation,

&HeapFragValue,

sizeof(HeapFragValue));

위 코드를 수행하면 Default Heap이 LFH로 바뀐다. 이제 그냥 new delete 쓰면 자동으로 LFH의

정책에 따라 메모리가 할당된다.

Page 6: Memory & object pooling

Windows LFHWindows Internals의 LFH에 대한 설명 - 멀티 스레드에서 잘 동작하는 이유?

... The LFH addresses these issues(* fragmentation) by using the core heap manager and look-aside lists. The Windows heap manager implements an automatic tuning algorithm that can enable the LFH by default under certain conditions, such as lock contention or the presence of popular size allocations that have shown better performance with the LFH enabled. For large heaps, a significant percentage of allocations is frequently grouped in a relatively small number of buckets of certain sizes. The allocation strategy used by LFH is to optimize the usage for these patterns by efficiently handling same-size blocks.

To address scalability, the LFH expands the frequently accessed internal structures to a number of slots that is two times larger than the current number of processors on the machine. The assignment of threads to these slots is done by an LFH component called the affinity manager. Initially, the LFH starts using the first slot for heap allocations; however, if a contention is detected when accessing some internal data, the LFH switches the current thread to use a different slot. Further contentions will spread threads on more slots. These slots are controlled for each size bucket to improve locality and minimize the overall memory consumption. ...

라고 설명하고 있으나 솔직히 무슨 소리인지 정확히 이해를 못하겠다. 자주 접근되는 내부 구조체에

대해 해당 슬롯에 대한 스레드의 대입(assignment)에서 lock contention이 일어나면 다른

슬롯을 쓰게 해주며, 슬롯들은 각 사이즈의 버킷에 의해 제어된다고 한다(지역성과 메모리 소모

최적화를 위해). 근데 슬롯이랑 내부 구조체, affinity manager가 당최 정확히 뭔지...

Page 7: Memory & object pooling

Windows LFH아무튼 LFH 성능은 굉장히 좋다고 합니다. (구승모 교수님 오피셜)

Page 8: Memory & object pooling

tcmalloc / jemalloctcmalloc은 이름부터 thread cache malloc으로, 각 스레드별 메모리 관리자와 중앙 메모리 관리

자를 구분하여 관리한다. 작은 크기(32kb)의 메모리 할당은 스레드별 메모리 관리자에서 할당하

고, 그보다 큰 크기의 메모리 할당은 중앙 메모리 관리자로부터 받아오도록 만들어서 작은 메모리의

할당이 잦게 일어나는 상황에서는 heap의 lock 때문에 발생하는 성능 저하가 없어서 좋은 효율을

보인다.

jemalloc은 크게 Arena와 Thread cache라는 두 개의 핵심 구조로 이루어져 있다. Arena는 메모

리를 작은 덩어리로 잘라 각 스레드에게 나누어 준 영역이다. 즉 전체 힙에서 각각의 스레드가 쓸 영

역을 미리 금을 그어 정해놓고 쓰도록 만드는 것이다. 따라서 각 스레드가 쓰는 영역이 나뉘어 있기

때문에 동기화 문제가 발생하지 않는다. Thread cache는 tcmalloc의 그것과 똑같다. 작은 크기의

할당의 경우 tcmalloc처럼 Arena 영역을 살피지 않고 자신이 갖고 있는 Thread cache에 바로 할

당하게 함으로써 성능을 향상시킨다. 큰 맥락이나 구조는 tcmalloc이랑 비슷한 것 같다.

Page 9: Memory & object pooling

Object Pooling풀링이라는 개념은 기본적으로 비용이 큰 작업인 할당과 해제를 반복적으로 수행하면서 생기는 성

능 저하를 줄이기 위해 고안된 것이다. 어차피 자원이라는 건 한 번 할당하면 재활용할 수 있는 건데

뭐하러 매번 다시 할당하고 해제하냐! 미리 쓸 만큼 자원을 확보해놓고 그걸 돌려가면서 쓰자. 정도

로 보면 될 듯 하다. 따라서 할당과 해제가 굉장히 빈번하게 일어나는 경우에 풀을 쓰면 성능 향상 효

과를 많이 볼 수 있을 것이다.

오브젝트 풀링은 위의 풀링을 객체에 대해 적용시킨 개념이다. 미리 일정한 양만큼의 객체를 생성해

두고(pool), 거기서 필요한 만큼 가져다 쓰고 그걸 다 쓰면 삭제하는 게 아니라 다시 풀에 반납하고,

그걸 다시 꺼내서 쓰고... 를 반복해서 할당과 해제에 드는 비용을 줄이는 것이다.

Page 10: Memory & object pooling

Memory Pooling오브젝트 풀링과 마찬가지로 이번엔 풀링을 메모리에 적용시킨 개념이다. 미리 쓸 만큼의 메모리를

할당받은 다음(pool), 거기서 필요한 만큼 메모리를 꺼내서 쓰고 다 쓰면 다시 반납하고를 반복하는

것이다. 이렇게 하면 메모리를 할당하고 해제하는 시스템 콜이 훨씬 줄어들게 되므로 성능 상에서 이

점을 많이 얻을 수 있다.

단, 메모리 풀링은 오브젝트 풀링보다는 고려해야할 사항이 많다. 같은 크기들로만 구성된 오브젝

트 풀과 다르게 메모리 풀에서는 서로 다른 크기의 메모리들이 할당되고 해제될 수 있다. 그 때문에

heap에서와 마찬가지로 fragmentation 문제가 발생할 수 있기 때문에 메모리 관리 등에 대해 더

많은 신경을 써야한다.

Page 11: Memory & object pooling

STL Allocator보통 STL을 쓸 때 우리는 아래와 같이 코드를 작성한다.

std::vector<int> vi;

하지만 사실 여기에는 생략된 게 있다. 바로 allocator 지정이다. allocator 타입을 지정하지 않을

경우 STL은 내부적으로 기본 allocator를 이용해서 메모리를 할당하고 해제하는 작업을 진행하는

데, 이건 메모리 풀을 이용하거나 하는 방식이 아니기 때문에 fragmentation 등의 문제가 발생할

수 있다. 따라서 STL에서 메모리 풀을 적용하고 싶다면 직접 custom allocator를 작성하여 해당

STL 컨테이너가 직접 작성한 메모리 풀을 기반으로 할당 해제 동작을 진행하도록 만들어야 한다.

Page 12: Memory & object pooling

STL Allocatorcustom allocator의 조건

• 템플릿이어야 한다.

STL 자체가 임의 타입에 대해 동작하므로 당연한 이야기다. 템플릿으로 작성되어야 임의 타입에 대

해 제대로 동작하는 allocator가 될 수 있을 것이다.

• pointer(T*)와 reference(T&)라는 타입을 제공해야한다.

STL 컨테이너 내부 구현에서 해당 타입들을 사용한다.

• 비정적 데이터 멤버를 가질 수 없다.

메모리 할당을 담당하는 Allocator가 개별 상태(멤버 변수)를 가진다는 것 자체가 말이 안 된다.

• 중첩 template을 제공해야한다(rebind)

list 등의 node에서 지정된 T타입보다 큰 메모리가 필요할 수 있기 때문이다.

Page 13: Memory & object pooling

STL Allocator예시

template <typename T>class customAllocator{public: using value_type = T; using pointer = value_type*; using const_pointer = const value_type*; using reference = value_type&; using const_reference = const value_type&;

template <typename U> struct rebind { using other = customAllocator<U>; };

void construct(pointer p, const T& t); // 생성자 호출 void destroy(pointer p); // 소멸자 호출 T* allocate(size_t n); // 메모리 할당 ( 메모리 풀에서 가져오기 ) - 인자는 객체 개수 기준 void deallocate(T* ptr, size_t n); // 메모리 할당 해제 ( 반납 )

private:};

allocate ,deallocate 함수를 적절히 메모

리 풀에서 메모리를 가져오고 반납하도록 만

들면 메모리 풀을 이용하는 STL container

를 이용할 수 있는 것이다