Workspace IndexAlgorithms › Day 46

Benchmark Methodology — Warm-up, Variance, and Automatic Regression Detection TODO

Algorithms · Day 46 / 100 · C. Concurrency & Performance Engineering (Day 36-51)

Concept

A benchmark isn't about measuring one number — it's an experiment that estimates a distribution of measurements. JIT compilation, caches, branch predictors, and connection pools all mean early runs differ from steady state, so the warm-up period should be excluded from measurement. What users actually feel is closer to the median and tail percentiles (p95, p99) than the average, and reporting the variance across repeated runs is what makes comparing two versions valid. Regression detection comes down to whether the difference from a baseline exceeds the noise band, so on noisy environments like shared CI, you either widen the threshold generously or use relative comparisons within the same run.

A benchmark that ignores warm-up and variance lets real performance regressions through while flagging harmless changes as regressions — and eventually nobody trusts the results anymore.

Code & Formula

# 벤치마크 방법론 — 워밍업 구간을 제외하고, 분산까지 함께 보고해 회귀를 판정한다.
# 노이즈 범위(표준편차 기반 임계값)를 넘어선 차이만 "회귀"로 인정한다.

import random
import statistics

random.seed(7)

def run_samples(n, base_ms, warmup=3):
    """워밍업 n_warmup개는 버리고, 정상 상태 표본만 반환."""
    raw = [base_ms + random.gauss(0, base_ms * 0.05) for _ in range(n + warmup)]
    # 초반 워밍업 구간은 JIT/캐시 예열 때문에 더 느리다고 가정
    for i in range(warmup):
        raw[i] *= 1.6
    return raw[warmup:]

def summarize(samples):
    mean = statistics.mean(samples)
    stdev = statistics.stdev(samples)
    p95 = sorted(samples)[int(len(samples) * 0.95)]
    return mean, stdev, p95

def is_regression(baseline, candidate, z_threshold=2.0):
    """두 집단 평균 차이가 결합 표준오차의 z_threshold배를 넘으면 회귀로 판정."""
    m0, s0, _ = summarize(baseline)
    m1, s1, _ = summarize(candidate)
    se = ((s0 ** 2) / len(baseline) + (s1 ** 2) / len(candidate)) ** 0.5
    z = (m1 - m0) / se if se else float("inf")
    return z > z_threshold, z

baseline = run_samples(30, base_ms=10.0)
candidate_ok = run_samples(30, base_ms=10.2)   # 무해한 변경
candidate_bad = run_samples(30, base_ms=13.0)  # 실제 회귀

for name, cand in [("무해한 변경", candidate_ok), ("실제 회귀", candidate_bad)]:
    m0, s0, p95_0 = summarize(baseline)
    m1, s1, p95_1 = summarize(cand)
    flagged, z = is_regression(baseline, cand)
    print(f"[{name}] baseline mean={m0:.2f}ms p95={p95_0:.2f}ms | "
          f"candidate mean={m1:.2f}ms p95={p95_1:.2f}ms | z={z:.2f} -> "
          f"{'REGRESSION' if flagged else 'OK'}")

Exercise

Run a single function many times with Go's testing.B, extract percentiles and standard deviation, and tabulate how much the numbers change with and without warm-up.

Practical Connection

Contract gas consumption is deterministic, so regressions can be pinned to an exact value, but off-chain matching-engine and API latency have to be managed as distributions — Verex's CI needs two different kinds of regression gates for these two cases.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

벤치마크 방법론 TODO

Algorithms · Day 46 / 100 · C. 동시성·성능 엔지니어링 (Day 36–51)

워밍업·분산·회귀 자동 감지

개념

벤치마크는 하나의 수치를 재는 일이 아니라 측정값의 분포를 추정하는 실험이다. JIT 컴파일, 캐시, 분기 예측기, 커넥션 풀 때문에 초기 실행은 정상 상태와 다르므로 워밍업 구간을 측정에서 제외해야 한다. 사용자 체감은 평균보다 중앙값과 꼬리 분위수(p95, p99)에 가깝고, 반복 실행 간 분산을 함께 보고해야 두 버전의 비교가 성립한다. 회귀 감지는 기준선 대비 차이가 노이즈 범위를 넘는지의 판단이므로, 공유 CI처럼 흔들리는 환경에서는 임계값을 넉넉히 잡거나 같은 실행 안에서의 상대 비교를 쓴다.

워밍업과 분산을 무시한 벤치마크는 실제 성능 회귀를 통과시키고 무해한 변경을 회귀로 오탐해서, 결국 아무도 결과를 믿지 않게 된다.

코드 · 수식

# 벤치마크 방법론 — 워밍업 구간을 제외하고, 분산까지 함께 보고해 회귀를 판정한다.
# 노이즈 범위(표준편차 기반 임계값)를 넘어선 차이만 "회귀"로 인정한다.

import random
import statistics

random.seed(7)

def run_samples(n, base_ms, warmup=3):
    """워밍업 n_warmup개는 버리고, 정상 상태 표본만 반환."""
    raw = [base_ms + random.gauss(0, base_ms * 0.05) for _ in range(n + warmup)]
    # 초반 워밍업 구간은 JIT/캐시 예열 때문에 더 느리다고 가정
    for i in range(warmup):
        raw[i] *= 1.6
    return raw[warmup:]

def summarize(samples):
    mean = statistics.mean(samples)
    stdev = statistics.stdev(samples)
    p95 = sorted(samples)[int(len(samples) * 0.95)]
    return mean, stdev, p95

def is_regression(baseline, candidate, z_threshold=2.0):
    """두 집단 평균 차이가 결합 표준오차의 z_threshold배를 넘으면 회귀로 판정."""
    m0, s0, _ = summarize(baseline)
    m1, s1, _ = summarize(candidate)
    se = ((s0 ** 2) / len(baseline) + (s1 ** 2) / len(candidate)) ** 0.5
    z = (m1 - m0) / se if se else float("inf")
    return z > z_threshold, z

baseline = run_samples(30, base_ms=10.0)
candidate_ok = run_samples(30, base_ms=10.2)   # 무해한 변경
candidate_bad = run_samples(30, base_ms=13.0)  # 실제 회귀

for name, cand in [("무해한 변경", candidate_ok), ("실제 회귀", candidate_bad)]:
    m0, s0, p95_0 = summarize(baseline)
    m1, s1, p95_1 = summarize(cand)
    flagged, z = is_regression(baseline, cand)
    print(f"[{name}] baseline mean={m0:.2f}ms p95={p95_0:.2f}ms | "
          f"candidate mean={m1:.2f}ms p95={p95_1:.2f}ms | z={z:.2f} -> "
          f"{'REGRESSION' if flagged else 'OK'}")

연습

함수 하나를 Go의 testing.B로 여러 차례 돌려 분위수와 표준편차를 뽑고, 워밍업 유무에 따라 수치가 얼마나 달라지는지 표로 정리하라.

실무 · Verex 연결

컨트랙트 가스 소비는 결정적이라 정확한 값으로 회귀를 고정할 수 있는 반면 오프체인 매칭 엔진과 API 지연은 분포로 관리해야 하므로, Verex CI에서 두 종류의 회귀 게이트를 다른 방식으로 설계해야 한다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 45. 프로파일링 심화47. eBPF로 프로덕션 관측 →