3. 틱 루프의 GC 압박, LOH, 풀 이중 반납
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 실시간 슈팅 게임 서버의 틱 루프(tick loop)다. 서버는 60Hz로 돈다.
// 매 틱마다:
// - 접속한 플레이어들의 입력 패킷을 처리하고
// - 각 룸의 상태 변화를 "스냅샷 메시지"로 만들어 브로드캐스트 큐에 넣는다.
// (룸 전체 엔티티 상태를 직렬화한 풀(full) 스냅샷이라 페이로드가 크다.)
// 메시지 객체는 GC 부담을 줄이려고 ObjectPool 로 재활용하도록 설계했다.
//
// 운영 중 증상: 가동 몇 분 뒤부터 "틱 시간"이 들쭉날쭉(스파이크)하고,
// GC Gen2 / LOH 컬렉션이 자주 뜬다는 제보가 있다. 풀을 썼는데도 그렇다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 틱 핫패스에서 힙 할당을 최소화한다(GC 압박 회피).
// - 메시지 객체는 풀에서 빌려 쓰고 처리 후 반납한다.
// - 처리량이 높아야 하며 GC 일시정지로 인한 틱 스파이크가 없어야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) GC 압박/틱 스파이크의 원인이 되는 할당들을 모두 지적하라.
// 2) (A)(B)(C)(D) 각 지점의 결함을 설명하라.
// 3) 풀 사용 버그(반납/고갈)와 대용량 버퍼 문제를 수정하라.
// ============================================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
namespace TickLoop
{
// 브로드캐스트할 스냅샷 메시지
public sealed class EventMessage
{
public int RoomId;
public int EventType;
public byte[] Payload = new byte[96 * 1024]; // (D) 고정 페이로드 버퍼
public int PayloadLen;
public void Reset()
{
RoomId = 0;
EventType = 0;
PayloadLen = 0;
}
}
public sealed class MessagePool
{
private readonly ConcurrentBag<EventMessage> _bag = new();
public EventMessage Rent()
{
if (_bag.TryTake(out var m))
return m;
return new EventMessage(); // 비면 새로 생성
}
public void Return(EventMessage m)
{
m.Reset();
_bag.Add(m);
}
}
public sealed class GameServer
{
private readonly MessagePool _pool = new();
private readonly ConcurrentQueue<EventMessage> _broadcastQueue = new();
private readonly List<int> _activeRooms = new() { 1, 2, 3, 4, 5 };
// 매 틱 호출 (60Hz)
public void Tick(int tickNo)
{
foreach (var roomId in _activeRooms)
{
// (A) 매 틱, 매 룸마다 로그 문자열을 만든다
string log = "tick " + tickNo + " room " + roomId + " processing";
LogVerbose(log);
var msg = _pool.Rent();
msg.RoomId = roomId;
msg.EventType = 7;
msg.PayloadLen = FillPayload(msg.Payload, roomId, tickNo);
_broadcastQueue.Enqueue(msg);
// (B) 통계 수집: object[] 에 담아 집계기로 넘김
RecordStat("room_event", roomId, tickNo);
}
// 브로드캐스트 처리
DrainBroadcast();
}
private void DrainBroadcast()
{
while (_broadcastQueue.TryDequeue(out var msg))
{
Broadcast(msg);
_pool.Return(msg);
// (C)
_pool.Return(msg);
}
}
private int FillPayload(byte[] buf, int roomId, int tickNo)
{
// 대충 몇 바이트만 채운다
buf[0] = (byte)roomId;
buf[1] = (byte)tickNo;
return 2;
}
private void Broadcast(EventMessage msg)
{
// 실제로는 소켓 전송. 여기선 생략.
}
// 통계: 키/값을 object 박스로 받아 집계
private static readonly Dictionary<string, long> s_stats = new();
private void RecordStat(string name, params object[] args)
{
// (B 계속)
lock (s_stats)
{
long sum = 0;
foreach (var a in args) sum += Convert.ToInt64(a);
s_stats.TryGetValue(name, out var prev);
s_stats[name] = prev + sum;
}
}
private void LogVerbose(string s)
{
// 운영에선 보통 꺼져 있지만 호출 자체는 매 틱 일어남
if (VerboseEnabled)
Console.WriteLine(s);
}
public bool VerboseEnabled = false;
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 실시간 슈팅 게임 서버의 틱 루프(tick loop)다. 서버는 60Hz로 돈다.
// 매 틱마다:
// - 접속한 플레이어들의 입력 패킷을 처리하고
// - 각 룸의 상태 변화를 "스냅샷 메시지"로 만들어 브로드캐스트 큐에 넣는다.
// (룸 전체 엔티티 상태를 직렬화한 풀(full) 스냅샷이라 페이로드가 크다.)
// 메시지 객체는 할당 비용을 줄이려고 ObjectPool 로 재활용하도록 설계했다.
//
// 운영 중 증상: 가동 몇 분 뒤부터 "틱 시간"이 들쭉날쭉(스파이크)하고,
// 가끔 크래시하거나 엉뚱한 룸의 페이로드가 섞여 나간다는 제보가 있다.
// 풀을 썼는데도 그렇다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 틱 핫패스에서 힙 할당을 최소화한다(할당자 압박/단편화 회피).
// - 메시지 객체는 풀에서 빌려 쓰고 처리 후 반납한다.
// - 처리량이 높아야 하며 할당 지연으로 인한 틱 스파이크가 없어야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 할당자 압박/틱 스파이크의 원인이 되는 할당들을 모두 지적하라.
// 2) (A)(B)(C)(D) 각 지점의 결함을 설명하라.
// 3) 풀 사용 버그(이중 반납/고갈)와 대용량 버퍼 문제를 수정하라.
// ============================================================================
#include <cstdint>
#include <cstdio>
#include <deque>
#include <map>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
// 브로드캐스트할 스냅샷 메시지
struct EventMessage
{
int RoomId = 0;
int EventType = 0;
// (D) 고정 페이로드 버퍼: 매 메시지마다 96KB를 힙에 들고 있다
std::vector<std::uint8_t> Payload = std::vector<std::uint8_t>(96 * 1024);
int PayloadLen = 0;
void Reset()
{
RoomId = 0;
EventType = 0;
PayloadLen = 0;
}
};
class MessagePool
{
public:
// 풀에서 빌리기: 비면 새로 생성
EventMessage* Rent()
{
std::lock_guard<std::mutex> lk(mtx_);
if (!free_.empty())
{
EventMessage* m = free_.back();
free_.pop_back();
return m;
}
return new EventMessage(); // 비면 새로 할당 (96KB 신규)
}
void Return(EventMessage* m)
{
m->Reset();
std::lock_guard<std::mutex> lk(mtx_);
free_.push_back(m); // (C) 중복 반납을 막지 않는다
}
private:
std::mutex mtx_;
std::vector<EventMessage*> free_;
};
class GameServer
{
public:
// 매 틱 호출 (60Hz)
void Tick(int tickNo)
{
for (int roomId : activeRooms_)
{
// (A) 매 틱, 매 룸마다 로그 문자열을 만든다
std::ostringstream oss;
oss << "tick " << tickNo << " room " << roomId << " processing";
LogVerbose(oss.str());
EventMessage* msg = pool_.Rent();
msg->RoomId = roomId;
msg->EventType = 7;
msg->PayloadLen = FillPayload(msg->Payload, roomId, tickNo);
broadcastQueue_.push_back(msg);
// (B) 통계 수집: 문자열 키로 매번 조회/삽입
RecordStat("room_event", roomId + tickNo);
}
// 브로드캐스트 처리
DrainBroadcast();
}
private:
void DrainBroadcast()
{
while (!broadcastQueue_.empty())
{
EventMessage* msg = broadcastQueue_.front();
broadcastQueue_.pop_front();
Broadcast(msg);
pool_.Return(msg);
// (C)
pool_.Return(msg);
}
}
int FillPayload(std::vector<std::uint8_t>& buf, int roomId, int tickNo)
{
// 대충 몇 바이트만 채운다
buf[0] = static_cast<std::uint8_t>(roomId);
buf[1] = static_cast<std::uint8_t>(tickNo);
return 2;
}
void Broadcast(EventMessage* /*msg*/)
{
// 실제로는 소켓 전송. 여기선 생략.
}
// (B 계속) 통계: 문자열 키/값 누적
void RecordStat(const std::string& name, long value)
{
std::lock_guard<std::mutex> lk(statMtx_);
stats_[name] += value;
}
void LogVerbose(const std::string& s)
{
// 운영에선 보통 꺼져 있지만 호출 자체는 매 틱 일어남
if (verboseEnabled_)
std::printf("%s\n", s.c_str());
}
MessagePool pool_;
std::deque<EventMessage*> broadcastQueue_;
std::vector<int> activeRooms_{1, 2, 3, 4, 5};
std::mutex statMtx_;
std::map<std::string, long> stats_;
public:
bool verboseEnabled_ = false;
};
int main()
{
GameServer server;
for (int t = 0; t < 5; ++t)
server.Tick(t);
std::printf("done\n");
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.