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++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 대규모 동시접속 서버의 "워커 스레드별 처리량 카운터"다.
// N개의 워커 스레드가 패킷을 처리하면서, 자기 인덱스의 카운터를 1씩 올린다.
// 카운터는 워커마다 독립적이라 "락도 필요 없고 경합도 없다"고 판단해
// 배열 하나에 나란히 모아 두었다. 모니터 스레드가 가끔 합계를 읽는다.
//
// 운영/벤치 중 증상: 워커 수를 1→2→4→8로 늘려도 전체 처리량이
// 기대만큼 선형으로 오르지 않는다. 심지어 스레드를 늘리면 단일 스레드보다
// 느려지는 구간도 관측된다. CPU는 100% 가까이 쓰는데 throughput이 안 난다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 각 워커는 자기 카운터만 갱신한다(논리적으로 공유 데이터 없음).
// - 카운터 갱신은 핫패스이며 절대적으로 빨라야 한다.
// - 코어 수에 비례해 처리량이 확장(scale)되어야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) "공유도 락도 없는데" 왜 스레드를 늘려도 확장되지 않는가?
// 2) (A)(B) 지점이 성능에 어떤 영향을 주는지 하드웨어 관점에서 설명하라.
// 3) 확장되도록 자료구조를 수정하라(측정 방법 포함).
// ============================================================================
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <thread>
#include <vector>
// 워커별 통계 구조체
struct WorkerStat
{
// (A) 여러 카운터가 한 구조체에 모여 있다
std::uint64_t packets = 0;
std::uint64_t bytes = 0;
std::uint64_t errors = 0;
};
class ThroughputCounters
{
public:
explicit ThroughputCounters(int n) : stats_(n) {}
// 워커 t 가 자기 통계를 갱신 (핫패스)
inline void OnPacket(int t, std::uint64_t sz)
{
stats_[t].packets += 1; // 자기 슬롯만 건드림
stats_[t].bytes += sz;
}
std::uint64_t TotalPackets() const
{
std::uint64_t sum = 0;
for (auto& s : stats_) sum += s.packets;
return sum;
}
private:
// (B) WorkerStat 들을 배열에 저장
std::vector<WorkerStat> stats_;
};
int main()
{
const int workers = std::thread::hardware_concurrency();
ThroughputCounters counters(workers);
const std::uint64_t iters = 50'000'000ULL;
auto t0 = std::chrono::steady_clock::now();
std::vector<std::thread> threads;
threads.reserve(workers);
for (int t = 0; t < workers; ++t)
{
threads.emplace_back([&counters, t, iters]() {
for (std::uint64_t i = 0; i < iters; ++i)
counters.OnPacket(t, 128);
});
}
for (auto& th : threads) th.join();
auto t1 = std::chrono::steady_clock::now();
double sec = std::chrono::duration<double>(t1 - t0).count();
std::printf("workers=%d total=%llu time=%.3fs throughput=%.1f Mops/s\n",
workers,
(unsigned long long)counters.TotalPackets(),
sec,
(workers * iters) / sec / 1e6);
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.