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.
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/.