KoreanFoodie's Study
[C++ 게임 서버] 4-9. SendBuffer Pooling 본문
[C++ 게임 서버] 4-9. SendBuffer Pooling
핵심 :
1. SendBuffer 를 보낼 때마다 메모리 할당을 하지 말고, Memory Pooling 기법을 이용해서 큰 초기에 1 번만 할당을 하는 기법을 사용해 보자.
2. 실제 Session 에서는 SendBufferManager 를 이용해 버퍼를 사용할 준비(Open 함수)를 한다. SendBufferManager -> SendBufferChunk -> SendBuffer 순으로 계층이 잡혀 있다.
3. SendBufferChunkRef 타입의 인자를 각 쓰레드별로 TLS(Thread-Local Space)에 잡아 두면, 각 쓰레드 끼리 Chunk 에 접근하는 데 있어 경합을 벌일 필요가 없어 효율적이다. 또한 Lock-Free 설계와도 궁합이 좋다.
이번 시간에는, SendBuffer 와 관련하여 풀링을 이용한 최적화를 구현해 보겠다.
사실 이전에는 Send 를 할 때마다 SendBuffer 를 위한 메모리 할당을 했었는데...
int32 GameSession::OnRecv(BYTE* buffer, int32 len)
{
// Echo
cout << "OnRecv Len = " << len << endl;
SendBufferRef sendBuffer = MakeShared<SendBuffer>(4096);
sendBuffer->CopyData(buffer, len);
GSessionManager.Broadcast(sendBuffer);
return len;
}
여기서 보면, GameSession 클래스에서 OnRecv 가 불릴 때마다 4096 사이즈의 SendBuffer 를 만들어 주고 있다(4096 으로 임의로 정한 건, 일단은 임의로 큰 값을 정해서 할당을 한 것이라고 받아들이자).
하지만 SendBuffer 를 필요할 때마다 만들지 않고, 미리 큰 영역을 할당해 놓은 다음, 필요할 때마다 SendBuffer 를 일부 가져와서 쓰면 어떨까? 사실 그게 기본적인 메모리 풀링의 아이디어이긴 하다. 😅
즉, 이번에 우리가 만들 것은, 엄~청 큰 메모리 버퍼(SendBufferChunk 라고 하자)를 만들고, SendBuffer 가 필요할 때마다 잘라서 쓴 다음, 사용이 완료되면 반납하는(스마트 포인터 활용) 방식이다. 😉
일단, SendBuffer 를 담는 대빵(?), SendBufferChunk 를 정의할 것이다.
SendBufferChunk.h
/*--------------------
SendBufferChunk
--------------------*/
class SendBufferChunk : public enable_shared_from_this<SendBufferChunk>
{
enum
{
SEND_BUFFER_CHUNK_SIZE = 6000
};
public:
SendBufferChunk();
~SendBufferChunk();
void Reset();
SendBufferRef Open(uint32 allocSize);
void Close(uint32 writeSize);
bool IsOpen() { return _open; }
BYTE* Buffer() { return &_buffer[_usedSize]; }
uint32 FreeSize() { return static_cast<uint32>(_buffer.size()) - _usedSize; }
private:
Array<BYTE, SEND_BUFFER_CHUNK_SIZE> _buffer = {};
bool _open = false;
uint32 _usedSize = 0;
};
일단, SEND_BUFFER_CHUNK_SIZE 가 눈에 띈다. 이 녀석은 아래 Array 를 보면... 버퍼를 배열로 만들어서 처음에만 할당할 건데, 이 배열의 사이즈를 의미한다. 즉, 여기서는 6000 개 분량의 SendBuffer 크기만큼 배열로 한꺼번에 할당을 한 후, 이 녀석을 이용해 SendBuffer 가 필요한 경우마다 빌려줄 것이다.
여기서 흥미롭게 봐야 할 부분은 Open 과 관련된 부분인데... 아래 상세 구현을 보자.
SendBufferChunk.cpp
SendBufferChunk::SendBufferChunk()
{
}
SendBufferChunk::~SendBufferChunk()
{
}
void SendBufferChunk::Reset()
{
_open = false;
_usedSize = 0;
}
SendBufferRef SendBufferChunk::Open(uint32 allocSize)
{
ASSERT_CRASH(allocSize <= SEND_BUFFER_CHUNK_SIZE);
ASSERT_CRASH(_open == false);
if (allocSize > FreeSize())
return nullptr;
_open = true;
return ObjectPool<SendBuffer>::MakeShared(shared_from_this(), Buffer(), allocSize);
}
void SendBufferChunk::Close(uint32 writeSize)
{
ASSERT_CRASH(_open == true);
_open = false;
_usedSize += writeSize;
}
Reset 은 그냥 뭔가 보기만 해도 초기화의 향기가 나고, Close 는 사용을 완료했다는 느낌을 준다.
Open 은 ObjectPool 클래스를 이용해 allocSize 크기의 SendBuffer 를 만들어 준다. 혹여나 ObjectPool 클래스의 자체 MakeShared 함수를 까먹었을 수도 있는데, 이 녀석은 그냥 해당 클래스의 생성자의 인자와 동일한 녀석들을 넣어주면 된다.
참고로, SendBuffer 는 아래처럼 변형할 것이다 :
class SendBuffer
{
public:
SendBuffer(SendBufferChunkRef owner, BYTE* buffer, int32 allocSize);
~SendBuffer();
BYTE* Buffer() { return _buffer; }
int32 WriteSize() { return _writeSize; }
void Close(uint32 writeSize);
private:
BYTE* _buffer;
uint32 _allocSize = 0;
uint32 _writeSize = 0;
SendBufferChunkRef _owner;
};
/////////////////////////////////////
Implementation
/////////////////////////////////////
SendBuffer::SendBuffer(SendBufferChunkRef owner, BYTE* buffer, int32 allocSize)
: _owner(owner), _buffer(buffer), _allocSize(allocSize)
{
}
SendBuffer::~SendBuffer()
{
}
void SendBuffer::Close(uint32 writeSize)
{
ASSERT_CRASH(_allocSize >= writeSize);
_writeSize = writeSize;
_owner->Close(writeSize);
}
SendBuffer 의 생성자를 잘 보면... owner 로 SendBufferChunkRef 가 잡혀 있는 것을 볼 수 있는데, 바로 이 녀석이 아까 ObjectPool<SendBuffer>::MakeShared(shared_rrom_this(), Buffer(), allocSize); 에서의 첫 인자, 'shared_from_this()' 되시겠다.
즉, 메모리 풀링을 해주면서, 해당 SendBuffer 의 소유주가 현재의 SendBufferChunk 임을 확실히 해 주는 것이다. 😎
이제 위의 SendBuffer, SendBufferChunk 를 별도의 매니저로 관리해 주면 되는데...
SendBufferManager.h
class SendBufferManager
{
public:
SendBufferRef Open(uint32 size);
private:
SendBufferChunkRef Pop();
void Push(SendBufferChunkRef buffer);
static void PushGlobal(SendBufferChunk* buffer);
private:
USE_LOCK;
Vector<SendBufferChunkRef> _sendBufferChunks;
};
SendBufferManager 는, 또 다시 SendBufferChunkRef 의 벡터를 가지고 있다. 자세한 구현을 보면...
SendBufferManager.cpp
SendBufferRef SendBufferManager::Open(uint32 size)
{
if (LSendBufferChunk == nullptr)
{
LSendBufferChunk = Pop(); // WRITE_LOCK
LSendBufferChunk->Reset();
}
ASSERT_CRASH(LSendBufferChunk->IsOpen() == false);
// 다 썼으면 버리고 새거로 교체
if (LSendBufferChunk->FreeSize() < size)
{
LSendBufferChunk = Pop(); // WRITE_LOCK
LSendBufferChunk->Reset();
}
cout << "FREE : " << LSendBufferChunk->FreeSize() << endl;
return LSendBufferChunk->Open(size);
}
SendBufferChunkRef SendBufferManager::Pop()
{
{
WRITE_LOCK;
if (_sendBufferChunks.empty() == false)
{
SendBufferChunkRef sendBufferChunk = _sendBufferChunks.back();
_sendBufferChunks.pop_back();
return sendBufferChunk;
}
}
return SendBufferChunkRef(xnew<SendBufferChunk>(), PushGlobal);
}
void SendBufferManager::Push(SendBufferChunkRef buffer)
{
WRITE_LOCK;
_sendBufferChunks.push_back(buffer);
}
void SendBufferManager::PushGlobal(SendBufferChunk* buffer)
{
GSendBufferManager->Push(SendBufferChunkRef(buffer, PushGlobal));
}
뭔가 생소한, LSendBufferChunk 라는 것이 등장했다. 이 녀석은 무엇일까?
/* CoreTLS.cpp */
thread_local SendBufferChunkRef LSendBufferChunk;
사실 이 녀석은 Thread-Local 하게(각 쓰레드마다 별도로 할당) 선언된 SendBufferChunkRef 타입의 인자이다.
즉, Open 을 보면.. 현재 쓰레드의 Chunk 가 nullptr 면 Pop 을 통해 새로 Chunk 를 할당받고, 필요한 크기에 비해 여유공간이 부족해도 Chunk 를 새로 할당할 것임을 알 수 있다. 그리고 Chunk 할당이 끝났으면 해당 Chunk 에 대해 Open 을 한 번 더 불러서 내부적으로도 풀링 처리를 한다.
이제 Pop 을 보면... _sendBufferChunks 배열이 비어 있지 않다는 건, 뭔가 재활용할 수 있는 Chunk 가 있다는 것이므로 스택을 사용하듯이 이를 다시 꺼내준다. 물론 배열이 비어있다면(기존에 만들고 나서 다 써서 Array 로 돌아온 녀석이 없다면), 새로 만들게 된다. 이때 소멸자를 PushGlobal 로 잡아 주기 때문에, 사용이 완료된 Chunk 가 _sendBufferChunks 벡터로 되돌아 올 수 있는 것이다! 😮
실제로 디버깅을 해 보면,
Chunk 내부의 FREE 한 영역(미사용 영역)이 줄어들고 있음을 알 수 있다!
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 5-1. BufferHelper (0) | 2023.12.14 |
---|---|
[C++ 게임 서버] 4-10. PacketSession (0) | 2023.12.14 |
[C++ 게임 서버] 4-8. SendBuffer (0) | 2023.12.13 |
[C++ 게임 서버] 4-7. RecvBuffer (0) | 2023.12.12 |
[C++ 게임 서버] 4-6. Session #3 (0) | 2023.12.12 |