본문 바로가기

Library/C/C++

C++ Memory Pools and Angra

윈도우 7의 LFH(Low Fragmented Heap)의 성능은 매우 좋은 편에 속한다. 개발 플랫폼을 윈도우 7으로 한정한다면, 기본 메모리 할당기만 써도 성능에 손해보는 일은 없을 것이다. 그러나, 플랫폼에서 LFH와 같은 쓸만한 힙 매니저를 제공하지 않는다면, 효과적으로 메모리를 관리하기 위해 잘 구현된 메모리 할당기가 필요하다. 대표적으로, boost, ACE, Loki는 간단하게 쓸 수 있는 메모리풀을 제공한다.

먼저, boost::singleton_pool은 부스트 풀 라이브러리(boost pool library)에 기반한 싱글턴  타입의 메모리 할당기이다. SSS(Simple Segregated Storage)라는 메커니즘을 바탕으로, 대량의 순차 할당, 반환 및 무작위 할당, 반환과 같은 어떠한 경우라도 놀라운 성능을 보여준다. 실전에 투입할 생각이라면 우선적으로 부스트 풀 라이브러리를 추천한다. 부스트 풀 라이브러리는 일반적인 메모리풀과 달리, 다양한 크기의 메모리 할당 요청을 처리할 수 있다.

그리고, 크로스 플랫폼 네트워크 라이브러리로 유명한 ACE에서 제공하는 ACE_Dynamic_Cached_Allocator가 있다. 속도로만 따진다면 비교 대상 메모리풀 중 최고의 성능을 보여준다. 그 압도적인 성능을 봤을 때, 다른 어떤 메모리풀과 비교해도 이보다 더 빠르기는 힘들 것이다. 그러나, ACE가 제공하는 메모리 할당기를 쓰려면 코드 내에서 부가적인 초기화 작업을 해주어야 하며, 고정 크기의 메모리 할당 요청만 처리할 수 있다는 단점이 있다. 즉, 사전에 미리 정해진 크기의 데이터에 대해서만 메모리풀을 구성할 수 있다.

마지막으로 Loki::SmallObject가 있다. Loki::SmallObject는 boost와 마찬가지로 다양한 크기의 메모리 할당 요청을 처리할 수 있다. 이 메모리풀은 재미있는 특징이 있는데, 먼저 다른 메모리 할당기와 달리 공간적인 오버헤드는 매우 적다. 사실, 매우 적은게 아니라 아예 없다. 그리고, 이름이 의미하는대로 매우 작은 크기의 개체들을 다루는데 최적화되어 있다. 그러나, 공간적인 오버헤드를 제거한 대신 메모리풀 구성 단위(Loki::Chunk)가 가질 수 있는 최대 메모리 블록 수가 제한되며, 대량의 메모리 할당, 반환에 매우 취약하다. 또, 할당해야 하는 메모리 블럭 크기가 커질수록 성능은 크게 하락한다.

Loki::SmallObject가 최고의 성능을 보이는 경우는 매우 제한적이다. 첫째, 다루어야 하는 메모리 블록의 크기가 4 ~ 32 바이트 정도로 비교적 작은 크기여야 한다. 둘째, 만 단위 이상의 메모리 할당, 반환이 필요한 경우가 아닌, 수 천번의 비교적 적은 수의 요청이어야 한다. 셋째, 통설과 달리 Loki::SmallObject는 무작위 메모리 할당, 반환이 아닌, 순차적인 메모리 할당, 반환 요청의 경우에 최고의 성능을 보여준다. Loki::SmallObject는 마지막으로 할당이나 반환이 일어난 Chunk에 대한 포인터를 캐시로 가지며, 이와 같은 정책은 완전 무작위 할당, 반환이 주로 요청되는 경우 캐시 미스의 대가가 상당히 크다. 그러나, 위의 조건을 만족하는 상황에서 Loki::SmallObject는 boost와 비슷하거나 그 이상의 성능을 보여준다.

블로그지기가 직접 만든 Angra 메모리풀은 Loki::SmallObject를 개선한 것인데, 대부분의 상황에서는 Loki::SmallObject보다 훨씬 낫다. 위에서 언급한 Loki::SmallObject의 약점을 크게 개선했고, Loki::SmallObject의 개선판답게 Loki::SmallObject가 강점을 보이는 상황에서는 뛰어난 성능을 보여준다. 그러나, Loki::SmallObject의 근본적인 한계 때문에, 큰 크기의 대량의 메모리 블럭을 다루는 상황에서는 boost나 ACE에 비해 좋은 성능을 보여주지 못한다.

다음은 이들 메모리풀의 테스트 일부인데, 순차 할당, 반환 성능을 테스트한 것이다. 개인적인 사정 때문에 완전한 테스트 결과는 공개하지 못하지만, 간단하게 이들의 성능을 알아보기에는 충분할 것이다. VC 9.0을 사용해 컴파일 했지만, 테스트에 참여한 메모리풀들은 2개 이상의 컴파일러에서 컴파일 검증을 거친 것들이다. 연속적인 할당, 반환에 관한 테스트이며, 반복 횟수는 100000번, 5번 측정하여 평균을 냈다.




다음은 Angra와 Loki::SmallObject가 최고의 성능을 보이는 상황에서 나머지 메모리 할당자들과 비교한 것이다. 순차 할당 + 순차 반환 시간을 측정했으며, 반복 횟수는 2048회, 5번 측정한 평균이다. ACE_Dynamic_Cached_Allocator는 고정 크기 데이터만 할당할 수 있다는 점을 제외하면, 어느 상황에서나 최고의 성능을 보여준다. Angra나 Loki::SmallObject가 최고의 성능을 보이는 상황은 매우 제한적이다.




boost는 템플릿 라이브러리이기 때문에 빌드도 간편하고, 추가되는 용량도 매우 작다. ACE와 Loki, Angra는 라이브러리를 링크해야 하기 때문에 이 점에서 약간의 오버헤드가 있다. 라이브러리 크기는 Angra가 가장 작은데, Loki와 ACE는 다양한 기능을 제공하는 기타 컴포넌트를 포함하기 때문이다.


이와 같은 테스트 결과를 바탕으로, 블로그지기가 제안하는 메모리풀 선택 기준은 다음과 같다.

첫째, 특정 크기의 데이터에 대해서만 메모리풀을 구성해야 한다면, ACE_Dynamic_Cached_Allocator가 최고의 성능을 보여준다. 성능만이 선택의 기준이라면 ACE를 선택하라. 단, ACE는 바이트 정렬 문제 때문에 4 바이트 미만의 데이터에 대해서 메모리풀을 구성할 수 없다. boost, Loki, Angra는 이런 제한이 없다.

둘째, 다양한 크기의 데이터에 대해서, 모든 상황에서 쓸만한 실전 메모리풀을 원한다면 부스트 풀 라이브러리를 고려하라.

셋째, 크기가 작은 여러 개체들에 대해, 적은 횟수(천 단위)의 메모리 할당 및 반환 작업이 필요하다면 Loki::SmallObject도 괜찮은 선택이다. 특히, 커스터마이징이 필요하다면 일반화된 템플릿 단위 전략 기반의 Loki::SmallObject는 여기서 진가를 발휘할 것이다. Loki::SmallObject는 boost나 ACE와 달리 코드 한 줄의 수정만으로 적용이 가능하기 때문에 사용 편의성이 매우 높다. 그러나, Loki::SmallObject는 데이터 크기와 메모리 블럭 할당 및 반환 요청 횟수에 따른 성능 편차가 매우 크다.


블로그지기의 Angra 메모리풀은 Loki::SmallObject에 비해 데이터 크기에 따른 성능 편차가 매우 적다. 이것은 Loki::SmallObject의 메모리 구조를 개선했기 때문이다. 그러나, Loki::SmallObject의 공간적인 오버헤드를 최소화하려는 특징을 이어 받았기 때문에, 대량의 메모리 블럭을 처리하는 성능은 역시 약점이다. Angra 메모리풀은 거의 모든 경우 Loki::SmallObject보다 훨씬 나은 성능을 보여주지만, boost나 ACE에 비해 만족스럽지 못하다. 좋은 성능을 보이는 환경은 Loki::SmallObject보다 넓지만, Loki::SmallObject의 근본적인 한계 때문에 최고의 성능은 보이지 못한다. Angra 메모리풀 역시 템플릿 단위 전략 기반 메모리풀이며, Loki::SmallObject와 같은 수준의 사용 편의성을 제공한다.

다시 말하지만, Loki::SmallObject가 최고의 성능을 보이는 경우는 데이터 크기가 작아야 할 뿐만 아니라, 순차적인 메모리 할당 및 반환이 주로 일어나며, 이들의 요청 횟수가 비교적 적은 상황이다. Loki::SmallObject의 메모리 할당 요청 횟수에 대한 약점은 메모리 블럭 관리를 위한 공간적인 오버헤드를 제거한 대가다. Angra 메모리풀 역시 이와 마찬가지 상황에서 최고의 성능을 발휘하지만, Loki::SmallObject보다는 약간 폭 넓게 쓸 수 있다.




* 덧글(2010.5.31) : 위의 메모리풀 테스트는 싱글스레드 상황에서 테스트한 것이다. 테스트에 참여한 메모리풀들은 모두 멀티스레드를 고려하여 작성되었지만, 각각 자신만의 동기화 방법을 가지고 있기 때문에 일관된 성능 측정이 곤란하다. 각 라이브러리에서 제공하는 동기화 개체 성능도 메모리풀 성능을 측정하는데 포함될 수 있으므로, 순수한 메모리풀의 성능을 알아보는데 멀티스레드 상에서의 테스트는 적절하지 않다고 판단했다.

이 덧글은 게임코디에서의 링크를 살펴보다가 추가하게 된 것인데, LFH 상에서의 메모리풀 성능 향상 여부에 대한 글들도 발견하게 되었다. 블로그지기가 발견한 사실 역시 비슷한데, 글의 처음에 잠깐 언급되어 있다. 즉, LFH가 적용된 상황에서 메모리풀을 사용하면 오히려 성능이 하락한다. 놀랍게도, LFH가 적용된 상황에서 기본 할당자는 어떤 메모리풀보다 빠른 성능을 보여준다. 이 사실은 VC 9.0의 일반적인 릴리즈 옵션으로 컴파일한, 스탠드얼론 실행 파일로 성능을 검사하다가 발견하게 된 것이다. 메모리풀을 적용한 릴리즈 바이너리와, 그렇지 않은 바이너리를 VC IDE 외부에서 실행했을 때, 두 바이너리가 거의 비슷한 성능을 내는 것을 보고 보다 자세한 조사를 하다가 알게 된 것이다.