Workspace IndexAlgorithms › Day 45

Advanced Profiling — Flame Graphs, perf, and PMU Counters TODO

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

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)

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

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

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


한국어

프로파일링 심화 TODO

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

플레임그래프·perf·PMU 카운터

개념

프로파일링은 크게 코드에 훅을 심는 계측(instrumentation)과 주기적으로 실행 상태를 표본화하는 샘플링으로 나뉘고, perf는 커널의 perf_events를 통해 타이머나 PMU 이벤트 오버플로 시점에 인터럽트를 걸어 콜스택을 수집하는 샘플링 도구다. 플레임그래프는 수집된 스택들을 동일 접두사끼리 접어(fold) 그린 그림으로, y축은 스택 깊이이고 x축 너비는 그 스택이 표본에서 차지한 비율일 뿐 시간 축이 아니다. 따라서 넓은 프레임은 '오래 걸린 구간'이 아니라 '표본에서 자주 잡힌, 즉 CPU를 많이 점유한 코드'로 읽어야 한다. PMU는 CPU 내장 하드웨어 카운터로 cycles, instructions, cache-misses, branch-misses 같은 이벤트를 세며, 이로부터 IPC를 계산해 병목이 명령어 공급 쪽인지 메모리 접근 쪽인지 분기 예측 실패 쪽인지 구분할 수 있다. 스택 수집은 frame pointer 유지나 DWARF 언와인딩 같은 전제가 필요해서, 최적화 빌드에서 스택이 끊기면 그래프 자체가 왜곡된다는 점이 실무에서 가장 흔한 함정이다.

추측으로 최적화하면 대개 엉뚱한 곳을 고치게 되고, 특히 CPU 바운드와 메모리 바운드는 해법이 완전히 다르다. 카운터와 플레임그래프는 그 구분을 데이터로 강제한다.

코드 · 수식

# 프로파일링 심화 — 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)

연습

의도적으로 캐시 미스가 많은 배열 순회 프로그램과 분기 예측이 실패하는 프로그램을 각각 만들고, perf로 IPC와 cache-misses, branch-misses를 재서 두 병목이 카운터 상에서 어떻게 다르게 보이는지 확인하라.

실무 · Verex 연결

Go로 짠 인덱서나 매칭 엔진이 처리량 한계에 부딪혔을 때, 원인이 실제 계산량인지 자료구조의 메모리 지역성인지 판단하는 데 그대로 쓰인다.

Jayverse에서의 위치

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

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"

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

← 1097. 테일 레이턴시1099. 벤치마크 방법론 →