4. 워커별 카운터의 false sharing

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 대규모 동시접속 서버의 "워커 스레드별 처리량 카운터"다.
// N개의 워커 스레드가 패킷을 처리하면서, 자기 인덱스의 카운터를 1씩 올린다.
// 카운터는 워커마다 독립적이라 "락도 필요 없고 경합도 없다"고 판단해
// 배열 하나에 나란히 모아 두었다. 모니터 스레드가 가끔 합계를 읽는다.
//
// 운영/벤치 중 증상: 워커 수를 1→2→4→8로 늘려도 전체 처리량이
// 기대만큼 선형으로 오르지 않는다. 심지어 스레드를 늘리면 단일 스레드보다
// 느려지는 구간도 관측된다. CPU는 100% 가까이 쓰는데 throughput이 안 난다.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 각 워커는 자기 카운터만 갱신한다(논리적으로 공유 데이터 없음).
//  - 카운터 갱신은 핫패스이며 절대적으로 빨라야 한다.
//  - 코어 수에 비례해 처리량이 확장(scale)되어야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) "공유도 락도 없는데" 왜 스레드를 늘려도 확장되지 않는가?
//  2) (A)(B) 지점이 성능에 어떤 영향을 주는지 하드웨어 관점에서 설명하라.
//  3) 확장되도록 자료구조를 수정하라(측정 방법 포함).
// ============================================================================

using System;
using System.Diagnostics;
using System.Threading;

namespace Throughput
{
    // 워커별 통계 구조체
    public struct WorkerStat
    {
        // (A) 여러 카운터가 한 구조체에 모여 있다 (24바이트)
        public long Packets;
        public long Bytes;
        public long Errors;
    }

    public sealed class ThroughputCounters
    {
        // (B) WorkerStat 들을 배열에 빽빽하게 저장
        private readonly WorkerStat[] _stats;

        public ThroughputCounters(int n) { _stats = new WorkerStat[n]; }

        // 워커 t 가 자기 통계를 갱신 (핫패스)
        public void OnPacket(int t, long sz)
        {
            _stats[t].Packets += 1;   // 자기 슬롯만 건드림
            _stats[t].Bytes   += sz;
        }

        public long TotalPackets()
        {
            long sum = 0;
            for (int i = 0; i < _stats.Length; i++) sum += _stats[i].Packets;
            return sum;
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            int workers = Environment.ProcessorCount;
            var counters = new ThroughputCounters(workers);
            const long iters = 50_000_000L;

            var sw = Stopwatch.StartNew();

            var threads = new Thread[workers];
            for (int t = 0; t < workers; t++)
            {
                int idx = t;
                threads[t] = new Thread(() =>
                {
                    for (long i = 0; i < iters; i++)
                        counters.OnPacket(idx, 128);
                });
                threads[t].Start();
            }
            foreach (var th in threads) th.Join();

            sw.Stop();
            double sec = sw.Elapsed.TotalSeconds;
            Console.WriteLine(
                $"workers={workers} total={counters.TotalPackets()} " +
                $"time={sec:F3}s throughput={(workers * iters) / sec / 1e6:F1} Mops/s");
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.