-
[Protobuf 따라하기] 수신 데이터를 복사 없이 가공하기개발/C·C++ 2024. 11. 8. 17:28
고정 패킷 이후에 등장하는 가변 데이터는 많고 이를 vector로 관리하게 되면 어쩔 수 없이 복사 비용이 발생한다. 개발이 비교적 편한 건 장점이지만 성능 면에서는 단점이다. 이 단점을 개선하기 위해 포인터를 활용하면 유지보수성이나 가독성이 조금 떨어질 수 있지만 성능에서는 이점이 있으므로 트레이드-오프를 잘 생각해자.
고정 패킷을 읽는 건 쉽다. 그냥 형 변환을 하면 된다.
// 패킷 #pragma pack(push, 1) struct PacketTest { struct BuffListItem { uint64 buff_id; float remain_time; }; uint16 packet_size; // 공용 헤더 uint16 packet_id; // 공용 헤더 uint64 id; uint32 hp; uint16 attack; uint16 buffs_offset; uint16 buffs_count; } #pragma pack(pop) PacketHeader* header = reinterpret_cast<PacketTest*>(buffer);
사실 그다음 데이터를 읽는 것도 쉽다. 패킷 헤더에 buffs_offset이라는 정보가 있기 때문에 정확히 그만큼 포인터를 이동시킨 다음에 가변 데이터 타입의 포인터로 형 변환을 해서 읽으면 된다. 이를 얼마나 우아아게 하느냐가 중요하다. 지금 이 결과물은 우아하다고 할 수 있다.
PacketTest::Buffs buffs = header->GetBuffs(); for (int i = 0; i < buffs.GetCount(); ++i) { printf("buff id: %llu, remain: %f\\n", buffs[i].buff_id, buffs[i].remain_time); } for (auto it = buffs.begin(); it != buffs.end(); ++it) { printf("buff id: %llu, remain: %f\\n", it->buff_id, it->remain_time); } for (auto& buff : buffs) { printf("buff id: %llu, remain: %f\\n", buff.buff_id, buff.remain_time); }
인덱스 연산자를 재정의했고 iterator를 쓸 수 있게 관련한 api도 정의가 되어 있다. 그래서 마치 vector를 사용하듯 api를 사용한다. buffs 객체의 타입은 PacketTest::buffs인데 buffs는 using을 이용해 별칭을 붙인 것이고 풀네임은 PacketTest::PacketDataWrapper<BuffListItem>이다. PacketDataWrapper 클래스를 살펴보자. 참고로 이 클래스는 템플릿인데 굳이 템플릿으로 안 만들어도 상관은 없다.
template<typename T> class PacketDataWrapper { class Iterator { public: Iterator(T* data): _data(data) {} T& operator*() { return *_data; } T* operator->() { return _data; } bool operator!=(const Iterator& rhs) { return _data != rhs._data; } Iterator& operator++() { ++_data; return *this; } Iterator operator++(int32_t) { Iterator temp(_data); ++_data; return temp; } private: T* _data; }; public: PacketDataWrapper() : _data(nullptr), _count(0) {} PacketDataWrapper(T* data, uint16 count) : _data(data), _count(count) {} T& operator[](uint16 index) { ASSERT_CRASH(index < _count); return _data[index]; } uint16 GetCount() { return _count; } Iterator begin() { return Iterator(_data); } Iterator end() { return Iterator(_data + _count); } private: uint16 _count; T* _data; };
외부에는 BYTE* 데이터인 buffer를 PacketTest*으로 변환한 패킷 객체가 있는 걸 기억하자. 그렇기 때문에 PacketTest 구조체 안에서 this 포인터로 스스로에게 접근하고 buffs_offset을 이용해 가변 데이터에 접근할 수 있다. 때문에 PacketDataWrapper의 인수 있는 생성자를 통해 포인터를 받아 적절하게 가공하면 우리가 원하는 걸 얻을 수 있다. 처음부터 구현하는 마음으로 인덱스 연산자 먼저 살펴보자.
template<typename T> class PacketDataWrapper { public: PacketDataWrapper() : _data(nullptr), _count(0) {} PacketDataWrapper(T* data, uint16 count) : _data(data), _count(count) {} T& operator[](uint16 index) { ASSERT_CRASH(index < _count); return _data[index]; } uint16 GetCount() { return _count; } private: uint16 _count; T* _data; };
_data를 초기화할 때는 포인터를 가변 데이터의 시작 위치로 세팅을 할 것이기 때문에 비교적 손쉽게 구현할 수 있다. 인덱스를 잘못 썼을 때는 크래시를 내도록 하자. 물론 구현만 가지고 사용할 수 있는 것은 아니며 PacketTest 객체에서 사용할 수 있도록 추가 조치를 해줘야 한다.
#pragma pack(push, 1) struct PacketTest { // ... using Buffs = PacketDataWrapper<BuffListItem>; Buffs GetBuffs() { BYTE* data = reinterpret_cast<BYTE*>(this); data += buffs_offset; return Buffs(reinterpret_cast<BuffListItem*>(data), buffs_count); } }; #pragma pack(pop)
배열처럼 사용하는 부분이 해결된다.
PacketTest::Buffs buffs = pkt->GetBuffs(); for (int i = 0; i < buffs.GetCount(); ++i) { printf("buff id: %llu, remain: %f\\n", buffs[i].buff_id, buffs[i].remain_time); }
Iterator 구현은 생각보다 쉽다. 사용하는 쪽의 코드를 먼저 보자. vector와 동일하게 생각해도 된다.
for (auto it = buffs.begin(); it != buffs.end(); ++it) { printf("buff id: %llu, remain: %f\\n", it->buff_id, it->remain_time); } for (auto& buff : buffs) { printf("buff id: %llu, remain: %f\\n", buff.buff_id, buff.remain_time); }
Iterator를 정의할 때는, 동작에 필요한 api만 만들어주면 된다. Iterator 클래스는 vector와 동일하게 자료구조의 내부에 위치하겠다. 당장 눈에 보이는 api는 ++, ≠, →이며 범위 기반 반복문에서 필요한 건 [값]이기 때문에 *도 재정의해야 한다. 참조 변수는 값으로 초기화해야 하기 때문이다.
begin(), end() 함수는 PacketDataWrapper에서 만들어주면 된다. 차근차근 해보자. Iterator 클래스는 원본 데이터의 포인터를 받아야 하는데, 순회하면서 데이터에 접근할 수 있어야 하기 때문이다. 생성자로 받으면 된다. 참고로 Iterator를 구현할 때 인덱스는 쓰는 방법도 있지만, 개인적으로 직접 포인터 연산하는 게 간편하다 생각해 포인터를 이용했다.
template<typename T> class PacketDataWrapper { class Iterator { public: Iterator(T* data): _data(data) {} private: T* _data; };
*, → 먼저 해보자
T& operator*() { return *_data; } T* operator->() { return _data; }
*는 역참조 연산자이기 때문에 포인터가 가리키는 대상을 반환하면 되므로 참조 반환을 한다. →는 멤버 접근 연산자이고 포인터에 접근하기 때문에 포인터 반환을 한다. 그다음도 어렵지 않으니 한 번에 이어서 구현하자.
bool operator!=(const Iterator& rhs) { return _data != rhs._data; } Iterator& operator++() { ++_data; return *this; } Iterator operator++(int32_t) { Iterator temp(_data); ++_data; return temp; }
여기까지 했으면 begin(), end() 함수는 매우 간단하다.
Iterator begin() { return Iterator(_data); } Iterator end() { return Iterator(_data + _count); }
end()는 마지막 요소 다음을 가리키므로 _count를 더하면 된다.
이제 buffs 객체를 배열처럼 쓸 수 있고 iterator로 쓸 수 있게 됐다.
'개발 > C·C++' 카테고리의 다른 글
VirtualAlloc()의 메모리 할당에 대해 (0) 2024.10.12 64비트 시스템에서 메모리 정렬 경계에 대해 (0) 2024.10.11 포인터 붕괴와 붕괴가 아닌 것 (0) 2024.09.28 덧셈 연산 어셈블리 코드 간단 분석 (1) 2024.09.07 std::transform()에 대한 간단한 고찰 (2) 2024.09.04