 |
|
첫 번째 메모리 관리자 -- Complex 클래스, 단일 스레드 환경
이제까지 설명한 원리를 염두에 두고 첫 번째 메모리 관리자를 구현해 보자. 설명을 쉽게
진행하기 위해 이 단계에서 구현하는 메모리 관리자는 단일 스레드 환경에서
Complex
객체만을 할당하고 해제한다. 구체적으로는, 메모리 관리자 내에
Complex
객체 풀(pool)을 확보한 후 요청이 들어오면 풀에서 객체를 할당한다. 필요한 객체 수가
풀에 있는 객체 수를 초과하면 풀 크기를 늘인다. 프로그램이 반환하는 객체는 풀에 다시
넣는다.
그림 1
은
Complex
객체 풀을 표현한 모습이다.
그림 1. Complex 객체 풀
풀에서 각 블록은 두 가지 목표를 수행한다.
-
Complex
객체를 저장한다.
- 풀에서 자신을 다음 블록으로 연결한다.
Complex
클래스 내부에 포인터를 저장하는 방법은 바람직하지 못하다. 그랬다가는 프로그램이 점유하는
메모리 양이 전반적으로 증가한다. 대신,
Complex
클래스 내 private 자료를 구조체로 묶은 후
Complex
포인터와 union을 생성한다. 즉 풀에 있을 때는 다음 블록을 가리키는 포인터가 되고,
Complex
객체로 사용할 때는 실수와 복소수를 저장하는 독립적인 구조체가 된다.
Listing 5
는 수정한
Complex
클래스다.
Listing 5. 추가적인 메모리 부하 없이 Complex*를 저장하는 변경된 자료 구조
class Complex
{
public:
Complex (double a, double b): r (a), c (b) {}
private:
union {
struct {
double r; // Real Part
double c; // Complex Part
};
Complex* next;
};
};
|
그러나 이런 전략은 ‘사용자 편의성’이라는 설계 목표를 위반한다. 메모리 관리자를 통합하려면
원래
Complex
클래스를 크게 뜯어 고쳐야 하기 때문이다. 그래서
FreeStore
라는 클래스를 새로 추가한다. 풀에 속할 때는 포인터, 그 외에는 Complex 개체가 되는
클래스다.
Listing 6
은
FreeStore
클래스다.
Listing 6. FreeStore 객체 자료 구조
struct FreeStore
{
FreeStore* next;
};
|
그러면 풀은
FreeStore
객체로 이루어진 연결 리스트가 된다. 목록 내 각 요소는 다음 요소를 가리키며, 이 요소를
Complex
객체로 사용한다.
MemoryManager
클래스는 자유 풀에서 첫 번째 요소를 가리키는 포인터만 저장한다. 요소가 부족할 때 풀을
확장하는 메서드(expandPoolSize)와 프로그램이 종료할 때 풀을 해제하는
메서드(cleanup)는 private 메서드로 구현한다.
Listing 7
은
FreeStore
개념을 추가하여
Complex
클래스를 수정한 모습이다.
Listing 7. FreeStore 개념을 추가하여 Complex 클래스를 수정한
자료 구조
#include <sys/types.h>
class MemoryManager: public IMemoryManager
{
struct FreeStore
{
FreeStore *next;
};
void expandPoolSize ();
void cleanUp ();
FreeStore* freeStoreHead;
public:
MemoryManager () {
freeStoreHead = 0;
expandPoolSize ();
}
virtual ~MemoryManager () {
cleanUp ();
}
virtual void* allocate(size_t);
virtual void free(void*);
};
MemoryManager gMemoryManager;
class Complex
{
public:
Complex (double a, double b): r (a), c (b) {}
inline void* operator new(size_t);
inline void operator delete(void*);
private:
double r; // Real Part
double c; // Complex Part
};
|
다음은 메모리를 할당하는 의사코드다.
-
FreeStore 풀이 존재하지 않는다면 FreesStore 풀을 생성한 후 3단계로
이동한다.
-
FreeStore 풀에 요소가 바닥났다면 FreeStore 풀을 새로 생성한다.
-
FreeStore 풀에서 첫 번째 요소를 반환한 후 다음 요소를 자유 공간을 점유하는
첫 번째 요소로 기억한다.
다음은 메모리를 해제하는 의사코드다.
-
해제할 포인터의 next 필드 값을 현재 FreeStore 풀에서 자유 공간을 점유하는
첫 번째 요소로 설정한다.
-
해제할 포인터를 FreeStore 풀에서 자유 공간을 점유하는 첫 번째 요소로
저장한다.
Listing 8
은
Complex
클래스를 생성하고 해제하는
new
와
delete
연산을 코드로 보여준다.
Listing 9
는 FreeStore 풀을 확장하고 삭제하는 expandPoolSize와 cleanup 함수를
보여준다. 그래도 아직 문제가 남아있다. 무엇일까?
Listing 8. Complex 클래스를 위한 메모리 할당과 해제 코드
inline void* MemoryManager::allocate(size_t size)
{
if (0 == freeStoreHead)
expandPoolSize ();
FreeStore* head = freeStoreHead;
freeStoreHead = head->next;
return head;
}
inline void MemoryManager::free(void* deleted)
{
FreeStore* head = static_cast <FreeStore*> (deleted);
head->next = freeStoreHead;
freeStoreHead = head;
}
void* Complex::operator new (size_t size)
{
return gMemoryManager.allocate(size);
}
void Complex::operator delete (void* pointerToDelete)
{
gMemoryManager.free(pointerToDelete);
}
|
FreeStore 풀을 생성하는 방법은 다소 까다롭다. 편법처럼 보이지만
FreeStore*
포인터는
Complex
객체로도 사용한다는 사실을 명심해야 한다. 따라서
Listing 9
처럼
FreeStore*
요청이 들어오면
FreeStore*
크기와
Complex
크기 중 큰 값을 사용해서 할당해야 한다.
Listing 9. FreeStore 풀을 확장하고 삭제하는 코드
#define POOLSIZE 32
void MemoryManager::expandPoolSize ()
{
size_t size = (sizeof(Complex) > sizeof(FreeStore*)) ?
sizeof(Complex) : sizeof(FreeStore*);
FreeStore* head = reinterpret_cast <FreeStore*> (new char[size]);
freeStoreHead = head;
for (int i = 0; i < POOLSIZE; i++) {
head->next = reinterpret_cast <FreeStore*> (new char [size]);
head = head->next;
}
head->next = 0;
}
void MemoryManager::cleanUp()
{
FreeStore* nextPtr = freeStoreHead;
for (; nextPtr; nextPtr = freeStoreHead) {
freeStoreHead = freeStoreHead->next;
delete [] nextPtr; // remember this was a char array
}
}
|
 |
흥미로운 참고 사항 한 가지
FreeStore 요소는 여전히 전역
new
/
delete
연산자로 생성한다. 그러나 여기서
malloc
/
free
조합을 사용해도 좋다. 양쪽 경우를 따져봐서 평균 성능은 별다른 차이가 없었다.
|
|
이상으로
Complex
클래스 메모리를 자체적으로 할당하고 해제하는 메모리 관리자를 구현했다. 앞서 기준 성능 지표가
3.5초였다는 사실을 기억하는가? 우리 메모리 관리자로 테스트 프로그램을 돌리면 0.67초가
걸린다! 테스트 프로그램용 클라이언트 코드를 변경할 필요도 없다! 상당한 차이가 나지 않은가?
주된 이유는 두 가지다.
-
사용자 코드와 커널 코드 사이를 전환하는 횟수가 줄었다. 해제되는 메모리를 풀에
넣었다가 재사용하기 때문이다.
-
메모리 관리자가 단일 스레드 할당자라서 테스트 프로그램이라는 목적에 적합하다. 다중
스레드 환경은 나중에 고려한다.
앞서 설계에 아직 문제가 있다고 말했다. 프로그램에서
new
로
Complex
객체를 할당한 후 해제하지 않으면 메모리 누수가 생긴다. 물론, 컴파일러가 제공하는
new
/
delete
전역 연산을 사용해도 마찬가지다. 그러나 우리가 메모리 관리자를 구현하는 이유는 단순히 성능
때문만이 아니다. 메모리 관리자는 설계부터 메모리 누수를 방지해야 한다. 현재
new
/
cleanUp
함수는
new
로 할당한 후 명시적으로 해제한 메모리만 운영체제로 반환한다. 이러한 문제를 해결하려면
프로그램 초기화 시 더 큰 메모리 블록을 요청한 후 저장하는 수밖에 없다.
new
연산은 저장한 메모리 블록에서 필요한 만큼씩 할당하고,
cleanUp
함수는 메모리 블록에서 생성한 개발 객체가 아니라 저장한 메모리 블록을 몽땅 해제해야 한다.
|