9. 질문 — 멀티코어 확장성: 틱 루프 동시성 설계 (종합)
난이도 최상내 답안
모범답안
모범답안 — 멀티코어 확장성과 틱 루프 동시성 설계 (종합)
난이도: 최상
핵심 답변
코어 대비 성능이 안 오르는 건 대개 공유 상태 경합 때문이다: 락 경합(직렬화), false sharing(같은 캐시라인 핑퐁), 캐시 일관성 트래픽, 메모리 대역폭 포화, 그리고 암달의 법칙(직렬 구간이 상한을 만듦). 해법의 핵심은 공유를 없애는 것 — shared-nothing 액터/잡 큐 모델로 "한 룸은 한 스레드가 독점 처리"하면 락이 사라져 코어 수만큼 깔끔히 확장된다.
깊이 있는 설명
확장성을 깎는 원인들
- 락 경합: 여러 코어가 한 락을 두고 줄을 서면 그 구간은 사실상 직렬. 경합이 심하면 스핀/블로킹·컨텍스트 스위칭 비용까지 더해진다.
- false sharing: 서로 다른 코어가 논리적으론 다른 변수를 갱신해도 그 변수들이 같은 64B 캐시라인에 있으면, MESI 프로토콜이 라인 소유권을 코어 간에 계속 빼앗아(핑퐁) 성능이 급락.
- 캐시 일관성 트래픽: 공유 쓰기가 많을수록 무효화(invalidation) 메시지가 코어 수에 비례해 늘어난다.
- 메모리 대역폭: 모든 코어가 메모리를 두드리면 대역폭이 포화돼 코어를 더 줘도 못 쓴다.
- 암달의 법칙: 전체 중 직렬 구간 비율이 s면 최대 속도향상은 1/s로 묶인다. 예: 직렬 5%면 코어가 무한이어도 최대 20배.
shared-nothing / 잡 큐(액터) 모델
- 상태(룸/세션)를 소유자에게 가두고, 다른 스레드는 그 소유자의 큐에 **메시지(잡)**를 넣을 뿐 직접 건드리지 않는다.
- 각 액터는 자기 큐를 단일 스레드로 순차 처리 → 락 불필요, 데이터 레이스 불가.
- 코어마다 독립된 액터 집합을 배치하면 경합 없이 선형 확장에 가깝다.
틱 루프 설계
- 룸마다 고정 주기(예: 30/60Hz)로 시뮬레이션을 한 스레드가 진행. 입력 패킷은 큐에 쌓였다가 틱 시작 시 일괄 적용.
- 장점: 결정론·디버깅 용이·락 없음. 한계: 한 룸이 한 코어 성능에 묶임(아주 무거운 룸은 분할 필요), 룸 간 상호작용은 메시지 패싱이라 지연·복잡도 증가.
응용/실무 연결
- MMO: 존을 코어/프로세스로 분할(shared-nothing), 존 경계 이동은 핸드오프 프로토콜.
- 통계/카운터처럼 꼭 공유해야 하는 값은
Interlocked/코어별 누적 후 머지로 경합 제거. - 핫 카운터는 캐시라인 정렬(
alignas(64))로 false sharing 차단.
흔한 오답·함정
- "스레드를 많이 만들면 빨라진다" — 경합·스위칭으로 오히려 느려질 수 있다. 적정 병렬도는 코어 수 + 작업 성격(IO/CPU).
- 락을 잘게 쪼개면 항상 좋다는 착각 — 락이 많아지면 락 순서·데드락·오버헤드가 늘고, 차라리 공유 제거가 낫다.
- false sharing은 코드만 봐선 안 보인다 —
perf c2c같은 도구로 측정해야 드러남.
꼬리질문 대비
- Q. 액터 모델의 단점? 룸 간 통신이 메시지 패싱이 되어 직접 호출보다 지연·복잡도↑, 그리고 한 액터가 한 코어 성능 한계에 묶임.
- Q. NUMA가 확장성에 주는 영향? 다른 노드의 메모리 접근이 느리므로, 스레드와 데이터를 같은 NUMA 노드에 배치(affinity)해야 한다.
- Q. 직렬 구간을 줄이는 현실적 방법? 전역 락 제거, 공유 카운터를 코어별로 분산, 시작/종료 같은 배리어 최소화.