KoreanFoodie's Study
[C++ 게임 서버] 2-8. 메모리 풀 #3 본문
[C++ 게임 서버] 2-8. 메모리 풀 #3
핵심 :
1. 마이크로소프트에서는, LockFreeStack 을 구현해서 이미 제공하고 있다 : SLIST_ENTRY
2. LockFreeStack 은 실질적으로 Lock-Based 구현에 비해 엄청 빠르거나 한 건 아니지만, 일단 사용하게 되면 전문용어로 '간지'가 난다.
저번 시간에는 LockFreeStack 을 이용해 Memory Pool 을 사용할 때, 각각의 entry 에 불필요한 메타 정보를 담는 것을 방지할 수 있었다. 메타 정보는 Header 에만 있으면 되기 때문이다(엔트리의 갯수가 너무 많지만 않으면).
그런데 일반적으로는 우리가 직접 만든 것을 사용하지 않고, 윈도우에서 만들어준 SLIST_ENTRY 를 사용한다. 실제 코드를 보자.
typedef struct DECLSPEC_ALIGN(16) _SLIST_ENTRY {
struct _SLIST_ENTRY *Next;
} SLIST_ENTRY, *PSLIST_ENTRY;
#pragma warning(pop)
#else
typedef struct _SINGLE_LIST_ENTRY SLIST_ENTRY, *PSLIST_ENTRY;
#endif // _WIN64
#if defined(_AMD64_)
typedef union DECLSPEC_ALIGN(16) _SLIST_HEADER {
struct { // original struct
ULONGLONG Alignment;
ULONGLONG Region;
} DUMMYSTRUCTNAME;
struct { // x64 16-byte header
ULONGLONG Depth:16;
ULONGLONG Sequence:48;
ULONGLONG Reserved:4;
ULONGLONG NextEntry:60; // last 4 bits are always 0's
} HeaderX64;
} SLIST_HEADER, *PSLIST_HEADER;
이전 글에서 직접 만든 버전과 놀랍도록(?) 유사한 것을 알 수 있다! 사실 그 이유는, 저번 글에서 마소의 구현을 그대로 흉내낸 것이기 때문이다 🤣 실제로, 윈도우 버전도 DECLSPEC_ALIGN(16) 을 통해 메모리를 16 바이트 정렬시킴을 확인할 수 있다.
어쨋든, 이제 메모리풀에서 윈도우 버전의 SLIST_HEADER 를 사용해 보자. PSLIST_HEADER 는, SLIST_HEADER 의 포인터 타입을 PSLIST_HEADER 로 쓰겠다는 뜻이다. 코드를 살펴보자.
MemoryPool.h
enum
{
SLIST_ALIGNMENT = 16
};
/*-----------------
MemoryHeader
------------------*/
DECLSPEC_ALIGN(SLIST_ALIGNMENT)
struct MemoryHeader : public SLIST_ENTRY
{
// [MemoryHeader][Data]
MemoryHeader(int32 size) : allocSize(size) { }
static void* AttachHeader(MemoryHeader* header, int32 size)
{
new(header)MemoryHeader(size); // placement new
return reinterpret_cast<void*>(++header);
}
static MemoryHeader* DetachHeader(void* ptr)
{
MemoryHeader* header = reinterpret_cast<MemoryHeader*>(ptr) - 1;
return header;
}
int32 allocSize;
// TODO : 필요한 추가 정보
};
/*-----------------
MemoryPool
------------------*/
DECLSPEC_ALIGN(SLIST_ALIGNMENT)
class MemoryPool
{
public:
MemoryPool(int32 allocSize);
~MemoryPool();
void Push(MemoryHeader* ptr);
MemoryHeader* Pop();
private:
SLIST_HEADER _header;
int32 _allocSize = 0;
atomic<int32> _allocCount = 0;
};
이제 MemoryHeader 는 SLIST_ENTRY 를 상속한다. ALIGN 을 해 주는 것 말고는, 헤더에서 고칠 건 딱히 더 없다.
MemoryPool.cpp
/*-----------------
MemoryPool
------------------*/
MemoryPool::MemoryPool(int32 allocSize) : _allocSize(allocSize)
{
::InitializeSListHead(&_header);
}
MemoryPool::~MemoryPool()
{
while (MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&_header)))
::_aligned_free(memory);
}
void MemoryPool::Push(MemoryHeader* ptr)
{
ptr->allocSize = 0;
::InterlockedPushEntrySList(&_header, static_cast<PSLIST_ENTRY>(ptr));
_allocCount.fetch_sub(1);
}
MemoryHeader* MemoryPool::Pop()
{
MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&_header));
// 없으면 새로 만들다
if (memory == nullptr)
{
memory = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(_allocSize, SLIST_ALIGNMENT));
}
else
{
ASSERT_CRASH(memory->allocSize == 0);
}
_allocCount.fetch_add(1);
return memory;
}
일단 소멸자를 보자. 소멸자에서는, InterlockedPopEntrySList 를 통해 헤더가 valid 한지를 체크하면서, free 를 해 준다.
InterlockedPopEntrySList ... 같은 함수는, 마소에서 이미 정의된 함수를 그냥 갖다 쓰는 것이다 ㅋㅋ
WINBASEAPI
VOID
WINAPI
InitializeSListHead(
_Out_ PSLIST_HEADER ListHead
);
WINBASEAPI
PSLIST_ENTRY
WINAPI
InterlockedPopEntrySList(
_Inout_ PSLIST_HEADER ListHead
);
WINBASEAPI
PSLIST_ENTRY
WINAPI
InterlockedPushEntrySList(
_Inout_ PSLIST_HEADER ListHead,
_Inout_ __drv_aliasesMem PSLIST_ENTRY ListEntry
);
또한 free 대신 _aligned_free 를 사용하는데, 이는 우리가 메모리를 사용할때 16 바이트로 정렬할 것을 명시했으므로, 이에 맞게 해제도 정렬 규칙에 따른다고 이해하면 된다.
Push 와 Pop도.. 마찬가지로 API 교체 및 캐스팅만 제대로 해 주면 된다 😊
이제 실제로 메모리를 할당/해제하는 곳에서, 아주 약간만 수정을 해 주면 된다.
void* Memory::Allocate(int32 size)
{
MemoryHeader* header = nullptr;
const int32 allocSize = size + sizeof(MemoryHeader);
if (allocSize > MAX_ALLOC_SIZE)
{
// 메모리 풀링 최대 크기를 벗어나면 일반 할당
header = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(allocSize, SLIST_ALIGNMENT));
}
else
{
// 메모리 풀에서 꺼내온다
header = _poolTable[allocSize]->Pop();
}
return MemoryHeader::AttachHeader(header, allocSize);
}
보면 header 를 할당하는 부분에서 _aligned_malloc 을 사용하는 것, 그리고 인자로 SLIST_ALIGNMENT 를 사용하는 것을 알 수 있다. SLIST_ALIGNMENT 는 16 으로 우리가 define 해 놓았으므로... 그냥, 정렬을 하며 malloc 을 해 준다고 이해하면 된다.
이제 Main 함수를 보면, 다음과 같이 간단히 사용할 수 있을 것이다 :
class Knight
{
public:
int32 _hp = rand() % 1000;
};
int main()
{
for (int32 i = 0; i < 5; i++)
{
GThreadManager->Launch([]()
{
while (true)
{
Knight* knight = xnew<Knight>();
cout << knight->_hp << endl;
this_thread::sleep_for(10ms);
xdelete(knight);
}
});
}
GThreadManager->Join();
}
이제 우리가 사용하려는 클래스가 우리가 만든 Custom SListEntry 클래스를 상속받도록 만들지 않아도 된다 😎
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 2-10. TypeCast (0) | 2023.09.19 |
---|---|
[C++ 게임 서버] 2-9. Object Pool (0) | 2023.09.15 |
[C++ 게임 서버] 2-7. 메모리 풀 #2 (0) | 2023.09.13 |
[C++ 게임 서버] 2-6. 메모리 풀 #1 (0) | 2023.09.11 |
[C++ 게임 서버] 2-5. STL Allocator (0) | 2023.08.25 |