커스텀 메모리 관리자 구현: C++에서의 효율적인 메모리 관리 🧠💻
메모리 관리는 프로그래밍에서 가장 중요한 요소 중 하나입니다. 특히 C++과 같은 저수준 언어에서는 메모리 관리가 프로그램의 성능과 안정성에 직접적인 영향을 미칩니다. 이 글에서는 C++에서 커스텀 메모리 관리자를 구현하는 방법에 대해 상세히 알아보겠습니다. 이는 단순히 이론적인 지식을 넘어 실제 프로젝트에 적용할 수 있는 실용적인 기술을 다룰 것입니다.
커스텀 메모리 관리자를 구현함으로써 얻을 수 있는 이점은 무엇일까요? 첫째, 메모리 할당과 해제의 오버헤드를 줄일 수 있습니다. 둘째, 메모리 단편화를 최소화할 수 있습니다. 셋째, 특정 애플리케이션에 최적화된 메모리 관리 전략을 구사할 수 있습니다. 이러한 이점들은 특히 게임 개발, 고성능 서버 애플리케이션, 임베디드 시스템 등에서 크게 빛을 발합니다.
이 글은 C++ 프로그래밍에 어느 정도 익숙한 개발자를 대상으로 합니다. 하지만 초보자들도 이 글을 통해 메모리 관리의 중요성과 기본 개념을 이해할 수 있을 것입니다. 재능넷의 '지식인의 숲' 메뉴에 등록될 이 글이, 여러분의 프로그래밍 실력 향상에 도움이 되기를 바랍니다. 자, 그럼 커스텀 메모리 관리자의 세계로 함께 들어가 볼까요? 🚀
1. 메모리 관리의 기초 📚
커스텀 메모리 관리자를 구현하기 전에, 먼저 메모리 관리의 기본 개념에 대해 이해해야 합니다. 이 섹션에서는 메모리 할당, 해제, 그리고 관련된 주요 개념들을 살펴보겠습니다.
1.1 메모리 구조
프로그램이 실행될 때, 운영 체제는 해당 프로그램에 메모리를 할당합니다. 이 메모리는 크게 다음과 같은 영역으로 나뉩니다:
- 코드 영역: 실행할 프로그램의 코드가 저장되는 영역
- 데이터 영역: 전역 변수와 정적 변수가 저장되는 영역
- 힙(Heap): 동적으로 할당되는 메모리 영역
- 스택(Stack): 함수 호출 시 생성되는 지역 변수와 매개변수가 저장되는 영역
이 중에서 우리가 주목해야 할 부분은 바로 힙(Heap)입니다. 커스텀 메모리 관리자는 주로 이 힙 영역을 효율적으로 관리하는 데 초점을 맞춥니다.
1.2 동적 메모리 할당
C++에서 동적 메모리 할당은 주로 new
연산자를 통해 이루어집니다. 예를 들어:
int* ptr = new int; // 정수 하나를 저장할 수 있는 메모리 할당
int* arr = new int[10]; // 10개의 정수를 저장할 수 있는 배열 할당
이렇게 할당된 메모리는 프로그래머가 명시적으로 해제하기 전까지 계속 유지됩니다. 메모리 해제는 delete
연산자를 사용합니다:
delete ptr; // 단일 객체 해제
delete[] arr; // 배열 해제
1.3 메모리 누수(Memory Leak)
메모리 누수는 할당된 메모리를 적절히 해제하지 않아 발생하는 문제입니다. 이는 프로그램이 장시간 실행될 경우 심각한 성능 저하나 크래시를 유발할 수 있습니다.
1.4 메모리 단편화(Memory Fragmentation)
메모리 단편화는 메모리 공간이 작은 조각들로 나뉘어 있어 큰 메모리 블록을 할당하기 어려운 상태를 말합니다. 이는 두 가지 유형으로 나눌 수 있습니다:
- 외부 단편화: 사용 가능한 메모리 공간이 여러 작은 조각으로 나뉘어 있는 상태
- 내부 단편화: 할당된 메모리 블록 내부에 사용되지 않는 공간이 있는 상태
커스텀 메모리 관리자를 구현할 때는 이러한 단편화를 최소화하는 전략을 고려해야 합니다.
1.5 메모리 정렬(Memory Alignment)
메모리 정렬은 데이터를 메모리에 저장할 때 특정 바이트 경계에 맞추는 것을 말합니다. 이는 CPU가 메모리에 더 효율적으로 접근할 수 있게 해주지만, 때로는 추가적인 메모리 공간을 필요로 할 수 있습니다.
struct AlignedStruct {
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
}; // 실제 크기: 12 bytes (패딩으로 인해)
위의 예에서, AlignedStruct
의 실제 크기는 7바이트가 아닌 12바이트가 됩니다. 이는 메모리 정렬을 위해 추가된 패딩 때문입니다.
1.6 스레드 안전성(Thread Safety)
멀티스레드 환경에서 메모리 관리자를 구현할 때는 스레드 안전성을 고려해야 합니다. 여러 스레드가 동시에 메모리를 할당하거나 해제하려고 할 때 발생할 수 있는 경쟁 조건(Race Condition)을 방지해야 합니다.
이러한 기본 개념들을 이해하는 것은 커스텀 메모리 관리자를 구현하는 데 있어 매우 중요합니다. 다음 섹션에서는 이러한 개념들을 바탕으로 실제 커스텀 메모리 관리자를 설계하고 구현하는 방법에 대해 알아보겠습니다. 🛠️
2. 커스텀 메모리 관리자 설계 🏗️
커스텀 메모리 관리자를 구현하기 위해서는 먼저 그 구조와 동작 방식을 설계해야 합니다. 이 섹션에서는 효율적인 메모리 관리자를 위한 주요 설계 고려사항과 전략에 대해 살펴보겠습니다.
2.1 메모리 풀(Memory Pool) 설계
메모리 풀은 미리 할당된 메모리 블록의 집합입니다. 이를 통해 동적 메모리 할당의 오버헤드를 줄이고, 메모리 단편화를 최소화할 수 있습니다.
메모리 풀 설계 시 고려해야 할 주요 사항들은 다음과 같습니다:
- 블록 크기: 풀 내의 각 메모리 블록의 크기를 결정해야 합니다. 이는 애플리케이션의 요구사항에 따라 다르겠지만, 일반적으로 자주 사용되는 크기의 객체들을 효율적으로 저장할 수 있는 크기로 선택합니다.
- 풀 크기: 전체 메모리 풀의 크기를 결정해야 합니다. 이는 예상되는 메모리 사용량과 시스템 리소스를 고려하여 결정합니다.
- 할당 전략: 메모리 블록을 어떤 순서로 할당할지 결정해야 합니다. 예를 들어, 첫 번째 적합(First Fit), 최적 적합(Best Fit) 등의 전략이 있습니다.
- 해제 전략: 사용이 끝난 메모리 블록을 어떻게 관리할지 결정해야 합니다. 즉시 재사용 가능한 상태로 만들 것인지, 아니면 특정 조건에서 운영체제에 반환할 것인지 등을 고려해야 합니다.
2.2 메모리 할당 알고리즘
효율적인 메모리 할당을 위해 다양한 알고리즘을 고려할 수 있습니다. 주요 알고리즘들은 다음과 같습니다:
- 첫 번째 적합(First Fit): 요청된 크기를 수용할 수 있는 첫 번째 가용 블록을 선택합니다.
- 최적 적합(Best Fit): 요청된 크기에 가장 근접한 가용 블록을 선택합니다.
- 최악 적합(Worst Fit): 가장 큰 가용 블록을 선택하고, 남은 공간을 새로운 가용 블록으로 만듭니다.
- 다음 적합(Next Fit): 첫 번째 적합과 유사하지만, 마지막으로 할당된 위치부터 검색을 시작합니다.
2.3 메모리 해제 및 병합
메모리 블록이 해제될 때, 인접한 가용 블록들을 병합하는 것이 중요합니다. 이를 통해 메모리 단편화를 줄이고 큰 메모리 블록의 할당을 용이하게 할 수 있습니다.
void merge_free_blocks() {
Block* current = head;
while (current != nullptr && current->next != nullptr) {
if (current->is_free && current->next->is_free) {
// 인접한 가용 블록 병합
current->size += current->next->size;
current->next = current->next->next;
} else {
current = current->next;
}
}
}
2.4 메모리 정렬 고려
메모리 정렬은 성능에 큰 영향을 미칠 수 있습니다. 특히 특정 하드웨어 아키텍처에서는 정렬되지 않은 메모리 접근이 상당한 성능 저하를 유발할 수 있습니다.
void* aligned_alloc(size_t size, size_t alignment) {
size_t total_size = size + alignment - 1 + sizeof(void*);
void* p = malloc(total_size);
if (p == nullptr) return nullptr;
void* aligned_p = (void*)(((uintptr_t)p + sizeof(void*) + alignment - 1) & ~(alignment - 1));
*((void**)aligned_p - 1) = p;
return aligned_p;
}
void aligned_free(void* p) {
free(*((void**)p - 1));
}
2.5 스레드 안전성 설계
멀티스레드 환경에서는 메모리 관리자의 스레드 안전성이 중요합니다. 이를 위해 다음과 같은 방법들을 고려할 수 있습니다:
- 뮤텍스 사용: 전역 뮤텍스를 사용하여 메모리 할당 및 해제 작업을 동기화합니다.
- 세분화된 잠금: 메모리 풀을 여러 구역으로 나누고, 각 구역마다 별도의 뮤텍스를 사용합니다.
- 락프리(Lock-free) 알고리즘: 원자적 연산을 사용하여 동기화 오버헤드를 줄입니다.
class ThreadSafeAllocator {
private:
std::mutex mtx;
// 기타 멤버 변수들...
public:
void* allocate(size_t size) {
std::lock_guard<:mutex> lock(mtx);
// 메모리 할당 로직...
}
void deallocate(void* ptr) {
std::lock_guard<:mutex> lock(mtx);
// 메모리 해제 로직...
}
};
2.6 메모리 사용 추적
효율적인 메모리 관리를 위해 메모리 사용을 추적하는 기능을 구현하는 것이 좋습니다. 이를 통해 메모리 누수를 탐지하고, 전반적인 메모리 사용 패턴을 분석할 수 있습니다.
struct MemoryStats {
size_t total_allocated;
size_t total_freed;
size_t current_usage;
size_t peak_usage;
};
class MemoryTracker {
private:
MemoryStats stats;
std::mutex mtx;
public:
void record_allocation(size_t size) {
std::lock_guard<:mutex> lock(mtx);
stats.total_allocated += size;
stats.current_usage += size;
stats.peak_usage = std::max(stats.peak_usage, stats.current_usage);
}
void record_deallocation(size_t size) {
std::lock_guard<:mutex> lock(mtx);
stats.total_freed += size;
stats.current_usage -= size;
}
MemoryStats get_stats() {
std::lock_guard<:mutex> lock(mtx);
return stats;
}
};
이러한 설계 고려사항들을 바탕으로, 다음 섹션에서는 실제 커스텀 메모리 관리자의 구현에 대해 자세히 알아보겠습니다. 각 컴포넌트를 어떻게 코드로 구현하고, 어떻게 이들을 조합하여 효율적인 메모리 관리 시스템을 만들 수 있는지 살펴보겠습니다. 🖥️
3. 커스텀 메모리 관리자 구현 💻
이제 앞서 설계한 내용을 바탕으로 실제 커스텀 메모리 관리자를 구현해보겠습니다. 이 섹션에서는 C++를 사용하여 기본적인 메모리 관리자를 구현하고, 점진적으로 기능을 추가하며 개선해 나가는 과정을 살펴보겠습니다.
3.1 기본 구조 설계
먼저, 메모리 블록을 표현하는 기본 구조체와 메모리 관리자 클래스의 기본 구조를 정의해보겠습니다.
struct MemoryBlock {
size_t size;
bool is_free;
MemoryBlock* next;
};
class MemoryManager {
private:
void* memory_pool;
MemoryBlock* free_list;
size_t pool_size;
public:
MemoryManager(size_t size);
~MemoryManager();
void* allocate(size_t size);
void deallocate(void* ptr);
private:
MemoryBlock* find_free_block(size_t size);
void split_block(MemoryBlock* block, size_t size);
void merge_free_blocks();
};
3.2 생성자 및 소멸자 구현
메모리 관리자의 생성자에서는 메모리 풀을 할당하고 초기화합니다. 소멸자에서는 할당된 메모리를 해제합니다.
MemoryManager::MemoryManager(size_t size) : pool_size(size) {
memory_pool = malloc(size);
if (!memory_pool) throw std::bad_alloc();
free_list = static_cast<MemoryBlock*>(memory_pool);
free_list->size = size - sizeof(MemoryBlock);
free_list->is_free = true;
free_list->next = nullptr;
}
MemoryManager::~MemoryManager() {
free(memory_pool);
}
3.3 메모리 할당 구현
메모리 할당 함수에서는 요청된 크기에 맞는 가용 블록을 찾아 할당합니다. 여기서는 첫 번째 적합(First Fit) 알고리즘을 사용하겠습니다.
void* MemoryManager::allocate(size_t size) {
MemoryBlock* block = find_free_block(size);
if (!block) return nullptr;
if (block->size > size + sizeof(MemoryBlock)) {
split_block(block, size);
}
block->is_free = false;
return reinterpret_cast<char*>(block) + sizeof(MemoryBlock);
}
MemoryBlock* MemoryManager::find_free_block(size_t size) {
MemoryBlock* current = free_list;
while (current) {
if (current->is_free && current->size >= size) {
return current;
}
current = current->next;
}
return nullptr;
}
void MemoryManager::split_block(MemoryBlock* block, size_t size) {
MemoryBlock* new_block = reinterpret_cast<memoryblock>(
reinterpret_cast<char>(block) + sizeof(MemoryBlock) + size
);
new_block->size = block->size - size - sizeof(MemoryBlock);
new_block->is_free = true;
new_block->next = block->next;
block->size = size;
block->next = new_block;
}
</char></memoryblock>
3.4 메모리 해제 구현
메모리 해제 함수에서는 주어진 포인터에 해당하는 블록을 찾아 해제하고, 가능한 경우 인접한 가용 블록들과 병합합니다.
void MemoryManager::deallocate(void* ptr) {
if (!ptr) return;
MemoryBlock* block = reinterpret_cast<memoryblock>(
static_cast<char>(ptr) - sizeof(MemoryBlock)
);
block->is_free = true;
merge_free_blocks();
}
void MemoryManager::merge_free_blocks() {
MemoryBlock* current = free_list;
while (current && current->next) {
if (current->is_free && current->next->is_free) {
current->size += sizeof(MemoryBlock) + current->next->size;
current->next = current->next->next;
} else {
current = current->next;
}
}
}
</char></memoryblock>
3.5 메모리 정렬 지원 추가
특정 정렬 요구사항을 지원하기 위해 정렬된 메모리 할당 함수를 추가할 수 있습니다.
void* MemoryManager::allocate_aligned(size_t size, size_t alignment) {
size_t total_size = size + alignment - 1;
void* ptr = allocate(total_size);
if (!ptr) return nullptr;
void* aligned_ptr = reinterpret_cast<void>(
(reinterpret_cast<uintptr_t>(ptr) + alignment - 1) & ~(alignment - 1)
);
if (aligned_ptr != ptr) {
MemoryBlock* block = reinterpret_cast<memoryblock>(
static_cast<char>(aligned_ptr) - sizeof(MemoryBlock)
);
block->size = size;
block->is_free = false;
}
return aligned_ptr;
}
</char></memoryblock></uintptr_t></void>
3.6 스레드 안전성 구현
멀티스레드 환경에서의 안전성을 보장하기 위해 뮤텍스를 사용하여 동기화를 구현합니다.
class ThreadSafeMemoryManager : public MemoryManager {
private:
std::mutex mtx;
public:
ThreadSafeMemoryManager(size_t size) : MemoryManager(size) {}
void* allocate(size_t size) override {
std::lock_guard<:mutex> lock(mtx);
return MemoryManager::allocate(size);
}
void deallocate(void* ptr) override {
std::lock_guard<:mutex> lock(mtx);
MemoryManager::deallocate(ptr);
}
};
3.7 메모리 사용 추적 기능 추가
메모리 사용 현황을 추적하기 위한 기능을 구현합니다.
class TrackedMemoryManager : public MemoryManager {
private:
size_t total_allocated;
size_t peak_usage;
size_t current_usage;
public:
TrackedMemoryManager(size_t size) : MemoryManager(size),
total_allocated(0), peak_usage(0), current_usage(0) {}
void* allocate(size_t size) override {
void* ptr = MemoryManager::allocate(size);
if (ptr) {
total_allocated += size;
current_usage += size;
peak_usage = std::max(peak_usage, current_usage);
}
return ptr;
}
void deallocate(void* ptr) override {
if (ptr) {
MemoryBlock* block = reinterpret_cast<memoryblock>(
static_cast<char>(ptr) - sizeof(MemoryBlock)
);
current_usage -= block->size;
}
MemoryManager::deallocate(ptr);
}
void print_stats() const {
std::cout << "Total allocated: " << total_allocated << " bytes\n"
<< "Peak usage: " << peak_usage << " bytes\n"
<< "Current usage: " << current_usage << " bytes\n";
}
};
</char></memoryblock>
3.8 성능 최적화
성능을 더욱 향상시키기 위해 몇 가지 최적화 기법을 적용할 수 있습니다.
- 프리 리스트 최적화: 자주 사용되는 크기의 블록들을 별도의 리스트로 관리합니다.
- 메모리 풀 분할: 큰 메모리 풀을 여러 개의 작은 풀로 나누어 관리합니다.
- 캐시 친화적 설계: 메모리 블록을 캐시 라인 크기에 맞게 정렬합니다.
class OptimizedMemoryManager : public MemoryManager {
private:
static constexpr size_t NUM_POOLS = 4;
static constexpr size_t POOL_SIZES[NUM_POOLS] = {16, 32, 64, 128};
MemoryBlock* free_lists[NUM_POOLS];
public:
OptimizedMemoryManager(size_t size) : MemoryManager(size) {
for (int i = 0; i < NUM_POOLS; ++i) {
free_lists[i] = nullptr;
}
}
void* allocate(size_t size) override {
for (int i = 0; i < NUM_POOLS; ++i) {
if (size <= POOL_SIZES[i]) {
if (free_lists[i]) {
MemoryBlock* block = free_lists[i];
free_lists[i] = block->next;
return reinterpret_cast<char>(block) + sizeof(MemoryBlock);
}
break;
}
}
return MemoryManager::allocate(size);
}
void deallocate(void* ptr) override {
if (!ptr) return;
MemoryBlock* block = reinterpret_cast<memoryblock>(
static_cast<char>(ptr) - sizeof(MemoryBlock)
);
for (int i = 0; i < NUM_POOLS; ++i) {
if (block->size <= POOL_SIZES[i]) {
block->next = free_lists[i];
free_lists[i] = block;
return;
}
}
MemoryManager::deallocate(ptr);
}
};
</char></memoryblock></char>
3.9 테스트 및 벤치마킹
구현한 메모리 관리자의 성능을 테스트하고 벤치마킹하는 것이 중요합니다. 다음은 간단한 벤치마크 코드 예시입니다.
#include <chrono>
#include <vector>
#include <iostream>
void benchmark_memory_manager(MemoryManager& mm, int num_allocations, size_t allocation_size) {
std::vector<void> ptrs;
ptrs.reserve(num_allocations);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_allocations; ++i) {
ptrs.push_back(mm.allocate(allocation_size));
}
for (void* ptr : ptrs) {
mm.deallocate(ptr);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<:chrono::microseconds>(end - start);
std::cout << "Time taken for " << num_allocations << " allocations and deallocations: "
<< duration.count() << " microseconds\n";
}
int main() {
const size_t POOL_SIZE = 1024 * 1024 * 10; // 10 MB
MemoryManager mm(POOL_SIZE);
OptimizedMemoryManager omm(POOL_SIZE);
std::cout << "Benchmarking basic MemoryManager:\n";
benchmark_memory_manager(mm, 10000, 32);
std::cout << "\nBenchmarking OptimizedMemoryManager:\n";
benchmark_memory_manager(omm, 10000, 32);
return 0;
}
</void></iostream></vector></chrono>
이러한 구현과 최적화를 통해 효율적이고 유연한 커스텀 메모리 관리자를 만들 수 있습니다. 실제 사용 시에는 애플리케이션의 특성과 요구사항에 맞게 추가적인 조정과 최적화가 필요할 수 있습니다.
다음 섹션에서는 이러한 커스텀 메모리 관리자를 실제 프로젝트에 적용하는 방법과 주의사항에 대해 알아보겠습니다. 또한, 더 나아가 고급 기능과 최적화 기법에 대해서도 살펴보겠습니다. 🚀
4. 실제 프로젝트 적용 및 고급 기법 🏭
이제 구현한 커스텀 메모리 관리자를 실제 프로젝트에 적용하는 방법과 더 나아가 고급 기법들에 대해 알아보겠습니다.
4.1 프로젝트 통합
커스텀 메모리 관리자를 프로젝트에 통합할 때는 다음과 같은 방법을 고려할 수 있습니다:
- 전역 new 및 delete 연산자 오버로딩: 프로젝트 전체에서 사용되는 메모리 할당/해제를 커스텀 관리자를 통해 처리합니다.
- 특정 클래스에 대한 메모리 관리: 메모리 사용이 빈번한 특정 클래스에 대해서만 커스텀 할당자를 사용합니다.
- 영역별 메모리 관리: 프로그램의 특정 영역 또는 모듈에 대해서만 커스텀 메모리 관리자를 적용합니다.
4.1.1 전역 new 및 delete 오버로딩
MemoryManager g_memory_manager(1024 * 1024 * 100); // 100 MB 풀
void* operator new(size_t size) {
void* ptr = g_memory_manager.allocate(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}
void operator delete(void* ptr) noexcept {
g_memory_manager.deallocate(ptr);
}
4.1.2 특정 클래스에 대한 메모리 관리
class MyClass {
public:
static void* operator new(size_t size) {
return g_memory_manager.allocate(size);
}
static void operator delete(void* ptr) {
g_memory_manager.deallocate(ptr);
}
// ... 클래스의 나머지 부분 ...
};
4.2 메모리 누수 탐지
커스텀 메모리 관리자를 사용하면 메모리 누수를 더 쉽게 탐지할 수 있습니다. 할당된 메모리 블록을 추적하고, 프로그램 종료 시 해제되지 않은 블록을 보고하는 기능을 추가할 수 있습니다.
class LeakDetectorMemoryManager : public MemoryManager {
private:
std::unordered_map<void size_t> allocated_blocks;
std::mutex mtx;
public:
void* allocate(size_t size) override {
void* ptr = MemoryManager::allocate(size);
if (ptr) {
std::lock_guard<:mutex> lock(mtx);
allocated_blocks[ptr] = size;
}
return ptr;
}
void deallocate(void* ptr) override {
if (ptr) {
std::lock_guard<:mutex> lock(mtx);
allocated_blocks.erase(ptr);
}
MemoryManager::deallocate(ptr);
}
void report_leaks() {
std::lock_guard<:mutex> lock(mtx);
if (!allocated_blocks.empty()) {
std::cout << "Memory leaks detected:\n";
for (const auto& pair : allocated_blocks) {
std::cout << "Address: " << pair.first << ", Size: " << pair.second << " bytes\n";
}
} else {
std::cout << "No memory leaks detected.\n";
}
}
};
</void>
4.3 메모리 단편화 최소화
메모리 단편화를 최소화하기 위해 다음과 같은 기법들을 적용할 수 있습니다:
- 버디 할당 시스템: 메모리 블록을 2의 거듭제곱 크기로 관리하여 외부 단편화를 줄입니다.
- 슬랩 할당자: 동일한 크기의 객체들을 효율적으로 관리합니다.
- 압축: 주기적으로 사용 중인 메모리 블록들을 한쪽으로 모아 큰 연속된 가용 공간을 확보합니다.
4.3.1 버디 할당 시스템 구현 예시
class BuddyAllocator {
private:
struct Block {
size_t size;
bool is_free;
Block* buddy;
};
std::vector<:vector>> free_lists;
void* memory_pool;
size_t pool_size;
public:
BuddyAllocator(size_t size) {
pool_size = next_power_of_two(size);
memory_pool = malloc(pool_size);
if (!memory_pool) throw std::bad_alloc();
size_t levels = log2(pool_size) + 1;
free_lists.resize(levels);
Block* root = new (memory_pool) Block{pool_size, true, nullptr};
free_lists[levels - 1].push_back(root);
}
void* allocate(size_t size) {
size = next_power_of_two(size + sizeof(Block));
int level = log2(size);
if (level >= free_lists.size() || free_lists[level].empty()) {
split_blocks(size);
}
if (level >= free_lists.size() || free_lists[level].empty()) {
return nullptr; // 할당 실패
}
Block* block = free_lists[level].back();
free_lists[level].pop_back();
block->is_free = false;
return reinterpret_cast<char>(block) + sizeof(Block);
}
void deallocate(void* ptr) {
if (!ptr) return;
Block* block = reinterpret_cast<block>(static_cast<char>(ptr) - sizeof(Block));
block->is_free = true;
merge_buddies(block);
}
private:
void split_blocks(size_t target_size) {
for (int i = free_lists.size() - 1; i >= 0; --i) {
if (!free_lists[i].empty()) {
Block* block = free_lists[i].back();
free_lists[i].pop_back();
while (block->size > target_size) {
size_t new_size = block->size / 2;
Block* buddy = reinterpret_cast<block>(
reinterpret_cast<char>(block) + new_size
);
buddy->size = new_size;
buddy->is_free = true;
buddy->buddy = block;
block->size = new_size;
block->buddy = buddy;
int new_level = log2(new_size);
free_lists[new_level].push_back(buddy);
}
free_lists[log2(block->size)].push_back(block);
return;
}
}
}
void merge_buddies(Block* block) {
while (block->buddy && block->buddy->is_free && block->size == block->buddy->size) {
Block* buddy = block->buddy;
int level = log2(block->size);
// Remove buddy from free list
auto it = std::find(free_lists[level].begin(), free_lists[level].end(), buddy);
if (it != free_lists[level].end()) {
free_lists[level].erase(it);
}
// Merge blocks
if (block > buddy) {
std::swap(block, buddy);
}
block->size *= 2;
block->buddy = buddy->buddy;
if (block->buddy) {
block->buddy->buddy = block;
}
}
int level = log2(block->size);
free_lists[level].push_back(block);
}
static size_t next_power_of_two(size_t n) {
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
}
static int log2(size_t n) {
return static_cast<int>(std::log2(n));
}
};
</int></char></block></char></block></char>
4.4 캐시 친화적 설계
현대 CPU의 캐시 시스템을 고려한 메모리 관리는 성능 향상에 큰 도움이 됩니다. 다음과 같은 기법들을 적용할 수 있습니다:
- 캐시 라인 정렬: 메모리 블록을 캐시 라인 크기(일반적으로 64바이트)에 맞춰 정렬합니다.
- 객체 패딩: 자주 접근하는 데이터 구조체의 크기를 캐시 라인 크기의 배수로 조정합니다.
- 메모리 프리페칭: 예측 가능한 메모리 접근 패턴에 대해 미리 메모리를 캐시로 로드합니다.
4.4.1 캐시 라인 정렬 예시
class CacheAlignedAllocator {
private:
static constexpr size_t CACHE_LINE_SIZE = 64;
public:
void* allocate(size_t size) {
size_t aligned_size = (size + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1);
void* ptr = aligned_alloc(CACHE_LINE_SIZE, aligned_size);
if (!ptr) throw std::bad_alloc();
return ptr;
}
void deallocate(void* ptr) {
free(ptr);
}
};
4.5 NUMA 인식 메모리 관리
NUMA(Non-Uniform Memory Access) 아키텍처를 가진 시스템에서는 메모리 접근 시간이 메모리의 물리적 위치에 따라 달라집니다. NUMA 인식 메모리 관리자는 이를 고려하여 최적의 성능을 제공할 수 있습니다.
#include <numa.h>
class NumaAwareAllocator {
public:
void* allocate(size_t size, int node) {
return numa_alloc_onnode(size, node);
}
void deallocate(void* ptr) {
numa_free(ptr, 0); // 크기를 0으로 지정하면 자동으로 계산됩니다.
}
};
</numa.h>
4.6 프로파일링 및 성능 분석
커스텀 메모리 관리자의 성능을 지속적으로 모니터링하고 개선하기 위해 프로파일링 도구를 사용하는 것이 중요합니다. 다음과 같은 도구들을 활용할 수 있습니다:
- Valgrind: 메모리 누수 및 접근 오류 탐지
- perf: Linux 시스템에서의 성능 프로파일링
- gperftools: Google의 성능 분석 도구
이러한 고급 기법들을 적용함으로써, 여러분의 커스텀 메모리 관리자는 더욱 강력하고 효율적으로 동작할 수 있습니다. 하지만 각 기법의 적용은 프로젝트의 특성과 요구사항에 따라 신중히 고려해야 합니다. 때로는 간단한 구현이 복잡한 최적화보다 더 나은 선택일 수 있음을 기억하세요.
이로써 커스텀 메모리 관리자 구현에 대한 심층적인 가이드를 마무리하겠습니다. 이 지식을 바탕으로 여러분만의 효율적인 메모리 관리 시스템을 구축하시기 바랍니다. 항상 성능과 안정성의 균형을 유지하며, 지속적인 테스트와 최적화를 통해 시스템을 개선해 나가세요. 행운을 빕니다! 🌟