Advanced Profiling — Flame Graphs, perf, and PMU Counters TODO
Concept
Profiling splits broadly into instrumentation, which hooks into code, and sampling, which periodically samples the running state; perf is a sampling tool that uses the kernel's perf_events to trigger an interrupt on a timer or PMU event overflow and collect the call stack at that moment. A flame graph is a picture built by folding collected stacks that share the same prefix together — the y-axis is stack depth, and the x-axis width is only the share of samples that stack accounted for, not a time axis. So a wide frame should be read as "code that got sampled often, i.e., occupied a lot of CPU," not as "a section that took a long time." The PMU is a set of hardware counters built into the CPU that count events like cycles, instructions, cache-misses, and branch-misses; from these you can compute IPC and tell whether a bottleneck is instruction supply, memory access, or branch misprediction. Stack collection depends on preconditions like maintaining the frame pointer or DWARF unwinding, and a broken stack in an optimized build — which distorts the graph itself — is the most common pitfall in practice.
Optimizing by guesswork usually fixes the wrong spot, and CPU-bound versus memory-bound problems have completely different fixes. Counters and flame graphs force that distinction to be made from data.
Code & Formula
# 프로파일링 심화 — perf 처럼 "일정 주기마다 인터럽트를 걸어 콜스택을 표본화"하는 걸 signal 타이머로 재현한다.
# 플레임그래프의 너비는 "오래 걸린 구간"이 아니라 "표본에서 자주 잡힌 스택(=CPU 를 많이 먹은 코드)"임에 유의.
import signal
from collections import Counter
samples = [] # 각 표본: 인터럽트가 걸린 순간의 콜스택(튜플)
def on_timer_tick(signum, frame): # perf_events 의 오버플로 인터럽트 핸들러에 해당
stack = []
f = frame
while f is not None:
stack.append(f.f_code.co_name)
f = f.f_back
samples.append(tuple(reversed(stack)))
def cache_miss_heavy(n): # 캐시 미스가 잦다고 가정한(=CPU 를 오래 점유하는) 함수
total = 0
for i in range(n):
total += i * i
return total
def branch_miss_heavy(n): # 분기 예측 실패가 잦다고 가정한 함수
total = 0
for i in range(n):
total += -i if i % 7 == 0 else i
return total
def workload():
cache_miss_heavy(3_000_000) # 실행 시간이 더 긴 쪽 -> 표본에서 더 넓은 프레임을 차지해야 정상
branch_miss_heavy(500_000)
signal.signal(signal.SIGVTALRM, on_timer_tick) # CPU(가상) 시간 기준 인터럽트 등록
signal.setitimer(signal.ITIMER_VIRTUAL, 0.001, 0.001) # 1ms 주기 샘플링 (perf -F 1000 과 같은 아이디어)
try:
workload()
finally:
signal.setitimer(signal.ITIMER_VIRTUAL, 0) # 타이머 해제
signal.signal(signal.SIGVTALRM, signal.SIG_DFL)
# 플레임그래프 접기(fold): 같은 스택 경로를 하나로 묶어 등장 횟수(=CPU 점유 비율 proxy)를 센다.
folded = Counter(";".join(s) for s in samples if s)
total_samples = sum(folded.values())
print("folded stacks (flamegraph 입력 포맷과 동일한 'stack;stack;...;count'):")
for stack, count in folded.most_common(5):
pct = count / total_samples * 100 if total_samples else 0
print(f" {count:>4} ({pct:4.1f}%) {stack}")
cache_related = sum(c for s, c in folded.items() if "cache_miss_heavy" in s)
branch_related = sum(c for s, c in folded.items() if "branch_miss_heavy" in s)
print("\ntotal samples captured:", total_samples)
print("cache_miss_heavy 가 차지한 표본 비율:", f"{cache_related / total_samples:.0%}" if total_samples else "n/a")
print("branch_miss_heavy 가 차지한 표본 비율:", f"{branch_related / total_samples:.0%}" if total_samples else "n/a")
print("-> 실행 시간이 더 긴 함수가 더 넓은 프레임(더 많은 표본)을 차지한다:",
cache_related >= branch_related)
docs/code/algorithms/algorithms-45.py
Exercise
Build one program that deliberately causes lots of cache misses through array traversal and another that deliberately causes branch mispredictions, then use perf to measure IPC, cache-misses, and branch-misses on each and see how the two bottlenecks look different in the counters.
Practical Connection
When a Go-based indexer or matching engine hits a throughput ceiling, this is exactly what's used to determine whether the cause is genuine computation volume or the data structure's memory locality.
Where it lands in Jayverse
- Verex: wire a flame-graph/perf capture into the CI step that runs before a matching-engine performance change ships. That turns "guess and fix" into a stored artifact — IPC, cache-miss and branch-miss counters checked in alongside the change that supposedly fixed throughput.
- Devnet: run the cache-miss/branch-mispredict pair as a load test against the indexer before scaling hardware. Distinguishing genuine load from bad memory locality decides whether the fix is a bigger machine or a data-structure change, and Devnet is the cheap place to run that test first.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| hook into | ~에 갈고리를 걸듯 파고들다, 끼어들다 · 계측(instrumentation)이 코드 내부에 직접 개입하는 방식을 말할 때. "instrumentation, which hooks into code" |
| fold (stacks) | 접어서 합치다 · 같은 접두사를 가진 호출 스택들을 하나로 겹쳐 그릴 때. "folding collected stacks that share the same prefix" |
| occupy (CPU) | (자원을) 차지하다 · 넓은 프레임이 실행 시간이 아니라 CPU 점유 비중을 뜻함을 강조할 때. "occupied a lot of CPU" |
| distort | 왜곡시키다 · 잘못된 전제가 결과 그래프 자체를 틀어지게 만들 때. "which distorts the graph itself" |
| guesswork | 어림짐작, 근거 없는 추측 · 데이터 없이 감으로 최적화하는 접근을 비판할 때. "optimizing by guesswork usually fixes the wrong spot" |
| throughput ceiling | 처리량 한계 · 시스템이 더 이상 처리량을 못 늘리는 상한선. "hits a throughput ceiling" |
| pitfall | 함정, 흔히 빠지는 실수 · 실무에서 가장 자주 걸려 넘어지는 지점을 가리킬 때. "the most common pitfall in practice" |
| PMU | 성능 모니터링 유닛(Performance Monitoring Unit) · CPU에 내장된 하드웨어 카운터 집합, 사이클·명령어·캐시미스 등을 셈. "The PMU is a set of hardware counters" |
| IPC | 사이클당 명령어 수(Instructions Per Cycle) · PMU 카운터로 계산해 병목이 명령어 공급·메모리·분기예측 중 무엇인지 가늠하는 지표. "from these you can compute IPC and tell" |
| DWARF | 디버그 정보 표준 포맷(DWARF) · 최적화된 바이너리에서도 스택 되감기(unwinding)를 가능하게 하는 심벌 정보 형식. "maintaining the frame pointer or DWARF unwinding" |
| perf_events | 리눅스 커널의 perf_events 서브시스템 · 타이머나 PMU 이벤트 오버플로우 시 인터럽트를 걸어 그 순간의 콜스택을 수집하게 하는 커널 기능. "uses the kernel's perf_events to trigger an interrupt" |
| flame graph | 플레임 그래프 · 같은 접두사의 콜스택들을 겹쳐 그려 CPU 점유 비중을 보여주는 시각화 기법(세로축은 스택 깊이, 가로축은 시간이 아니라 샘플 비중). "A flame graph is a picture built by folding" |
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/.