순환 큐 (Circular Queue) 구현하기 - Unreal에서 떼 왔슈

이창준, Changjoon Lee·2025년 7월 27일
0

Game Server Hyperion 🎮

목록 보기
3/14

프로젝트 : https://github.com/HoonInPark/ServerHyperion.git
본 포스트에 대한 내용은 feat/framesync 브랜치에 있다.

사담

아프다.
어제 여섯시간 내리 앉아서 머리가 멍할 정도로 집중하고 나니 김기 몸살이 났다.
나 수능 공부할 때 이후로 이 정도로 집중해 본 적이 없었다.
누워서 쉬려고 할 때도 고쳐야 할 점들이 자꾸 머릿속에서 맴돈다.
영화 듄에 나오는 인간 컴퓨터 아저씨가 된 기분이다.

문제

"무례하긴, 순회야" - 객체 풀과 순회 인터페이스 구현하기에서 내부 멤버를 deque로 변경했다.
근데 deque는 반복적으로 푸시/팝을 할 경우 동적할당이 일어난다.
누구인가, 누가 런타임에 동적할당을 하였어?

해결

순환큐를 만들고 deque대신 ObjPool의 멤버로 바꿔 넣어주자.

순환큐를 직접 만들 순 있지만, 귀찮다.
예전에 알고리즘 공부하며 만든 게 있지만, 못 미덥다 -> (못 미더운 코드)
그래서 Unreal에서 사용하는 TCircularQueue를 떼오려 한다.

핵심 아이디어는 TCircularQueue 내의 TCircularBuffer에서 쓰는 TArrayvector로 바꾸는 거다.

template<typename InElementType> 
class StlCircularBuffer
{
public:
	using ElementType = InElementType;
    // ...

private:
	uint32 IndexMask;
	vector<ElementType> Elements;
};

그리고 초기화되기 전에 빈 값을 가질 수 있도록 StlCircularQueueStlCircularBuffer의 생성자를 고쳐줬다.

StlCircularBuffer(uint32 Capacity = 0)
{
	if (Capacity < 0) return;
	if (Capacity > 0xffffffffU) return;

	Elements.resize(bit_ceil(Capacity));
	fill(Elements.begin(), Elements.end(), ElementType()); // TypeName{} makes it with its default constructor

	IndexMask = Elements.size() - 1;
}

template<class ...P>
StlCircularBuffer(uint32 Capacity = 0, P&&... _Params) 
{
	if (Capacity > 0xffffffffU) return;

	Elements.resize(bit_ceil(Capacity));
	fill(Elements.begin(), Elements.end(), ElementType(forward<P>(_Params)...));

	IndexMask = Elements.size() - 1;
}

원래 TCircularQueue는 기본생성자가 정의되지 않았었다.

그리고 다른 클래스의 멤버 변수로 쓰일 경우, 생성자에서 초기화할 때 대입연산을 사용할 수 있도록 수정했다.

__forceinline StlCircularBuffer& operator=(const StlCircularBuffer& _InCirBuf)
{
	if (this == &_InCirBuf) return *this;

	Elements = _InCirBuf.Elements;
	IndexMask = _InCirBuf.IndexMask;
	return *this;
}

이제,
멤버로 그냥 ObjPool<stOverlappedEx> m_SendDataPool;와 같이 선언하고
생성자에서 m_SendDataPool = ObjPool<stOverlappedEx>(64);와 같이 해줄 수 있다.
m_SendDataPool은 대입되기 전까진 깡통 객체인 것.

아래는 전체 완성 코드.

StlCircularBuffer.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#define SERVERHYPERION_EXPORT

#ifdef SERVERHYPERION_EXPORT
#define SERVERHYPERION_API __declspec(dllexport)
#else
#define SERVERHYPERION_API __declspec(dllimport)
#endif

#include <iostream>
#include <vector>
#include <bit>
#include <windows.h>
#include <utility>

using namespace std;

typedef unsigned int		uint32;

template<typename InElementType> 
class StlCircularBuffer
{
public:
	using ElementType = InElementType;

	StlCircularBuffer(uint32 Capacity = 0)
	{
		if (Capacity < 0) return;
		if (Capacity > 0xffffffffU) return;

		Elements.resize(bit_ceil(Capacity));
		fill(Elements.begin(), Elements.end(), ElementType()); // TypeName{} makes it with its default constructor

		IndexMask = Elements.size() - 1;
	}

	template<class ...P>
	StlCircularBuffer(uint32 Capacity = 0, P&&... _Params) 
	{
		if (Capacity > 0xffffffffU) return;

		Elements.resize(bit_ceil(Capacity));
		fill(Elements.begin(), Elements.end(), ElementType(forward<P>(_Params)...));

		IndexMask = Elements.size() - 1;
	}

	__forceinline StlCircularBuffer& operator=(const StlCircularBuffer& _InCirBuf)
	{
		if (this == &_InCirBuf) return *this;

		Elements = _InCirBuf.Elements;
		IndexMask = _InCirBuf.IndexMask;
		return *this;
	}

public:
	__forceinline ElementType& operator[](uint32 Index)
	{
		return Elements[Index & IndexMask];
	}

	__forceinline const ElementType& operator[](uint32 Index) const
	{
		return Elements[Index & IndexMask];
	}

public:
	__forceinline uint32 Capacity() const
	{
		return Elements.size();
	}

	__forceinline uint32 GetNextIndex(uint32 CurrentIndex) const
	{
		return ((CurrentIndex + 1) & IndexMask);
	}

	__forceinline uint32 GetPreviousIndex(uint32 CurrentIndex) const
	{
		return ((CurrentIndex - 1) & IndexMask);
	}

private:
	uint32 IndexMask;
	vector<ElementType> Elements;
};

StlCircularQueue.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#define SERVERHYPERION_EXPORT

#ifdef SERVERHYPERION_EXPORT
#define SERVERHYPERION_API __declspec(dllexport)
#else
#define SERVERHYPERION_API __declspec(dllimport)
#endif

#include <iostream>
#include <atomic>
#include "StlCircularBuffer.h"

using namespace std;

typedef unsigned int		uint32;
typedef signed int	 		int32;

template<typename T> 
class StlCircularQueue
{
public:
	using FElementType = T;

	template <typename T>
	__forceinline constexpr remove_reference_t<T>&& MoveTemp(T&& Obj) noexcept
	{
		using CastType = std::remove_reference_t<T>;

		static_assert(is_lvalue_reference_v<T>, "MoveTemp called on an rvalue");
		static_assert(!is_same_v<CastType&, const CastType&>, "MoveTemp called on a const object");

		return (CastType&&)Obj;
	}

	template<class ...P>
	StlCircularQueue(uint32 CapacityPlusOne = 0, P&&... _Params)
		: Buffer(CapacityPlusOne, forward<P>(_Params)...)
		, Head(0)
		, Tail(0)
	{
	}

	__forceinline StlCircularQueue& operator=(const StlCircularQueue& _InCirQ)
	{
		if (this == &_InCirQ) return *this;

		Buffer = _InCirQ.Buffer;
		Head.store(_InCirQ.Head.load());
		Tail.store(_InCirQ.Tail.load());

		return *this;
	}

	inline auto begin() const { return Buffer.begin(); }
	inline auto end() const { return Buffer.end(); }

public:
	uint32 Count() const
	{
		int32 Count = Tail.load() - Head.load();

		if (Count < 0)
		{
			Count += Buffer.Capacity();
		}

		return (uint32)Count;
	}

	bool Dequeue(FElementType& OutElement)
	{
		const uint32 CurrentHead = Head.load();

		if (CurrentHead != Tail.load())
		{
			OutElement = MoveTemp(Buffer[CurrentHead]);
			Head.store(Buffer.GetNextIndex(CurrentHead));

			return true;
		}

		return false;
	}

	bool Dequeue()
	{
		const uint32 CurrentHead = Head.load();

		if (CurrentHead != Tail.load())
		{
			Head.Store(Buffer.GetNextIndex(CurrentHead));

			return true;
		}

		return false;
	}
    
	void Empty()
	{
		Head.store(Tail.load());
	}

	bool Enqueue(const FElementType& Element)
	{
		const uint32 CurrentTail = Tail.load();
		uint32 NewTail = Buffer.GetNextIndex(CurrentTail);

		if (NewTail != Head.load())
		{
			Buffer[CurrentTail] = Element;
			Tail.store(NewTail);

			return true;
		}

		return false;
	}

	bool Enqueue(FElementType&& Element)
	{
		const uint32 CurrentTail = Tail.load();
		uint32 NewTail = Buffer.GetNextIndex(CurrentTail);

		if (NewTail != Head.load())
		{
			Buffer[CurrentTail] = MoveTemp(Element);
			Tail.store(NewTail);

			return true;
		}

		return false;
	}

	__forceinline bool IsEmpty() const
	{
		return (Head.load() == Tail.load());
	}

	bool IsFull() const
	{
		return (Buffer.GetNextIndex(Tail.load()) == Head.load());
	}

	bool Peek(FElementType& OutItem) const
	{
		const uint32 CurrentHead = Head.load();

		if (CurrentHead != Tail.load())
		{
			OutItem = Buffer[CurrentHead];

			return true;
		}

		return false;
	}

	const FElementType* Peek() const
	{
		const uint32 CurrentHead = Head.load();

		if (CurrentHead != Tail.load())
		{
			return &Buffer[CurrentHead];
		}

		return nullptr;
	}

private:
	StlCircularBuffer<FElementType> Buffer;
	atomic<uint32> Head;
	atomic<uint32> Tail;
};
profile
C++ Game Developer

0개의 댓글