C++ 프로파일러 비교: perf 화염 그래프, gprof, Valgrind Callgrind, VTune, Tracy
들어가며: 서버 병목 분석
병목 함수를 못 찾는 상황
실제 겪는 상황:
- 게임 서버가 8코어 중 5코어를 100%로 돌리는데, 어느 함수가 문제인지 모름
- perf report를 봐도 심볼이 ???로 나와서 분석 불가
- "캐시 미스가 많다"는 말만 들었는데, 어떻게 측정하는지 모름
- 실시간으로 프레임별 지연을 보고 싶은데 gprof로는 불가능
- 메모리 누수가 의심되는데 어디서 발생하는지 추적이 안 됨
- gprof로 프로파일했는데 호출 그래프가 부정확하다는 말을 들음
- Valgrind를 돌리니 30배 느려져서 실용성이 없다고 느낌
추가 시나리오: API 서버 CPU 100%인데 핸들러 불명, 24시간 후 메모리 2GB→8GB(누수?), O(n)인데 n이 커지면 선형보다 느림(캐시 의심). 기본 프로파일링(cpp-series-15-1)을 넘어서:
-
perf 심화: 화염 그래프, 캐시 이벤트, 호출 스택 해석
-
Intel VTune: CPU 파이프라인, 메모리 대역폭, 스레드 동기화 분석
-
Tracy: 실시간 프레임 프로파일링, 게임/실시간 앱에 최적화 이 글을 읽으면:
-
perf로 화염 그래프를 만들고 병목을 시각적으로 찾을 수 있습니다.
-
gprof로 호출 그래프와 플랫 프로파일을 얻을 수 있다 (한계 포함).
-
Valgrind(Callgrind·Cachegrind·Memcheck)로 메모리·캐시 분석을 할 수 있습니다.
-
VTune으로 캐시 미스, 분기 예측 실패를 정량 분석할 수 있습니다.
-
Tracy로 프레임별 지연을 실시간으로 모니터링할 수 있습니다.
-
프로덕션 환경에서 안전하게 샘플링하는 패턴을 적용할 수 있습니다. 요구 환경: C++17 이상, Linux (perf), Intel CPU (VTune), CMake (Tracy)
상황별 프로파일러 선택
언제 어떤 도구를 쓸까?
flowchart TD
A[성능 문제 발생] --> B{문제 유형}
B -->|CPU 병목| C{환경?}
B -->|메모리 누수/오류| D[Valgrind Memcheck]
B -->|캐시 효율| E[Valgrind Cachegrind]
C -->|Linux 서버| F{Intel CPU?}
C -->|게임/실시간 앱| G[Tracy]
F -->|예| H{심층 분석?}
F -->|아니오/AMD| I[perf]
H -->|예: 캐시/파이프라인| J[Intel VTune]
H -->|아니오| I
I --> K[화염 그래프]
G --> L[실시간 타임라인]
도구별 특징 비교
| 도구 | 오버헤드 | 프로덕션 | 강점 | 약점 |
|---|---|---|---|---|
| perf | 1~5% | ✅ 가능 | 무료, Linux 표준, 화염 그래프 | AMD에서 일부 이벤트 제한 |
| gprof | 5~15% | △ 가능 | 호출 그래프, 설치 간단 | 샘플링 부정확, 인라인 무시 |
| Valgrind | 10~50배 | ❌ 불가 | 메모리 누수, 캐시 시뮬레이션 | 매우 느림, 짧은 실행만 |
| VTune | 5~15% | △ 스테이징 | 캐시/파이프라인 심층 분석 | Intel 전용, 상용 |
| Tracy | 0.1~1% | △ 선택적 | 실시간, 프레임 단위 | 코드 수정 필요 |
프로파일링 워크플로우
sequenceDiagram
participant Dev as 개발자
participant Perf as perf
participant Valgrind as Valgrind
participant VTune as VTune
participant Tracy as Tracy
Dev->>Perf: 1. perf record (빠른 병목 탐색)
Perf->>Dev: 화염 그래프, 상위 함수
Dev->>Valgrind: 2. Memcheck (메모리 누수 의심 시)
Valgrind->>Dev: 누수 위치, 잘못된 접근
Dev->>VTune: 3. VTune (캐시/파이프라인 의심 시)
VTune->>Dev: 캐시 미스, 분기 예측 리포트
Dev->>Tracy: 4. Tracy (실시간 프레임 분석)
Tracy->>Dev: 프레임별 지연 타임라인
perf 심화: 화염 그래프와 캐시 프로파일링
perf record 고급 옵션
# 샘플링 주기: 99Hz (초당 99회) - 기본값, 병목 탐색에 적합
perf record -F 99 -g ./myapp
# 999Hz: 더 세밀한 샘플링 (오버헤드 증가)
perf record -F 999 -g ./myapp
# 호출 스택 깊이 32 (기본 127, 필요시 조정)
perf record -F 99 --call-graph dwarf,4096 ./myapp
# 특정 이벤트: 캐시 미스
perf record -e cache-misses -F 99 -g ./myapp
# CPU 코어 0,1만 프로파일 (멀티스레드 시 특정 코어)
perf record -C 0,1 -F 99 -g ./myapp
옵션 설명:
-F 99: 초당 99회 샘플 → 오버헤드 낮음, 대부분 병목 포착-g: 호출 그래프 수집 (화염 그래프에 필수)--call-graph dwarf: DWARF 디버그 정보로 스택 언와인딩 (정확도 ↑)-e cache-misses: 캐시 미스 이벤트 (L1/L2/L3 미스)
화염 그래프 생성 (완전한 예제)
# 1. perf 데이터 수집 (30초간 실행)
perf record -F 99 -g -- ./myapp
# 2. FlameGraph 스크립트 설치 (한 번만)
git clone https://github.com/brendangregg/FlameGraph
export PATH=$PATH:$(pwd)/FlameGraph
# 3. SVG 화염 그래프 생성
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
# 4. 브라우저에서 열기
open flamegraph.svg # macOS
xdg-open flamegraph.svg # Linux
perf stat: 하드웨어 이벤트 분석
# 기본 통계
perf stat ./myapp
# 캐시 이벤트 상세
perf stat -e cycles,instructions,cache-references,cache-misses,L1-dcache-loads,L1-dcache-load-misses ./myapp
# 반복 실행하여 평균
perf stat -r 5 ./myapp
출력 해석:
Performance counter stats for './myapp' (5 runs):
1,234.56 msec task-clock # CPU 시간
42 context-switches # 컨텍스트 스위칭 (많으면 스레드 전환 비용)
0 cpu-migrations # CPU 마이그레이션
128 page-faults # 페이지 폴트
3,456,789,012 cycles # CPU 사이클
2,345,678,901 instructions # 실행 명령어 수 (1.52 insn per cycle)
123,456,789 cache-references # 캐시 참조
12,345,678 cache-misses # 10.0% 캐시 미스율!
핵심 지표:
- IPC (Instructions Per Cycle):
instructions/cycles→ 1.0 이상이면 좋음, 0.5 이하면 메모리 대기 - 캐시 미스율:
cache-misses/cache-references→ 10% 이상이면 메모리 접근 패턴 의심
perf annotate: 소스 라인별 분석
# 특정 함수의 어셈블리 + 소스 라인별 샘플 수
perf annotate -s processData
# perf record 후
perf report
# 'a' 키로 annotate, 's'로 심볼별 정렬
gprof: 호출 그래프 플랫 프로파일
gprof 개요
gprof는 GCC와 함께 제공되는 플랫 프로파일러입니다. -pg 옵션으로 컴파일하면 실행 시 gmon.out을 생성하며, 이를 분석해 함수별 CPU 시간 비율과 호출 그래프를 보여줍니다. 레거시 환경이나 perf가 없는 시스템에서 유용하지만, 인라인 함수·공유 라이브러리에서 부정확할 수 있습니다.
gprof 완전한 사용법
# 1. -pg 옵션으로 컴파일 (최적화와 함께 사용 가능)
g++ -std=c++17 -O2 -pg -g -o myapp profile_target.cpp
# 2. 실행 (gmon.out 자동 생성)
./myapp
# 3. 플랫 프로파일 (함수별 시간 비율)
gprof myapp gmon.out
# 4. 호출 그래프만 보기
gprof -q myapp gmon.out
# 5. 플랫 프로파일만 (그래프 제외)
gprof -p myapp gmon.out
# 6. 출력을 파일로 저장
gprof myapp gmon.out > gprof_report.txt
gprof 출력 해석
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
45.23 2.15 2.15 1 2150.00 2150.00 sortData
28.10 3.48 1.33 1 1330.00 1330.00 fillRandom
12.30 4.07 0.59 1 590.00 590.00 processDataCacheUnfriendly
10.00 4.54 0.48 1 480.00 480.00 processDataCacheFriendly
핵심 컬럼:
- % time: 해당 함수가 전체 시간에서 차지하는 비율
- self seconds: 해당 함수 자체에서 소비한 시간
- calls: 호출 횟수
- total: 해당 함수 + 하위 호출 전체 시간
gprof 호출 그래프 예시
index % time self children called name
[1] 100.0 0.00 4.54 main [1]
2.15 0.00 1/1 sortData [2]
1.33 0.00 1/1 fillRandom [3]
[2] 47.4 2.15 0.00 1 sortData [2]
gprof 한계와 대안
| 한계 | 설명 | 대안 |
|---|---|---|
| 인라인 무시 | -O2 인라인된 함수는 부모에 합산됨 | perf (DWARF로 정확한 스택) |
| 공유 라이브러리 | .so 내부 호출 추적 부정확 | perf, VTune |
| 멀티스레드 | 스레드별 분리 안 됨 | perf -C, VTune Threading |
| 샘플링 주기 | 고정된 주기, 짧은 함수 놓침 | perf -F 조절 가능 |
Valgrind: Callgrind·Cachegrind·Memcheck
Valgrind 개요
Valgrind는 동적 바이너리 계측 도구입니다. 프로그램을 가상 CPU에서 실행해 메모리 접근·캐시·호출을 정밀 분석합니다. 10~50배 느려지므로 짧은 실행이나 단위 테스트에만 사용합니다.
Valgrind 도구 비교
| 도구 | 용도 | 출력 |
|---|---|---|
| Callgrind | CPU 프로파일링, 호출 횟수 | callgrind.out.*, KCachegrind로 시각화 |
| Cachegrind | L1/L2/L3 캐시 미스 시뮬레이션 | 캐시 통계 |
| Memcheck | 메모리 누수, 잘못된 접근 | 누수 리포트, 에러 위치 |
Callgrind: CPU 프로파일링
# 1. Callgrind로 실행 (기본)
valgrind --tool=callgrind ./myapp
# 2. 출력 파일: callgrind.out.<pid>
# 3. KCachegrind로 시각화 (GUI)
# qcachegrind callgrind.out.12345
# 4. 명령줄에서 요약 보기
callgrind_annotate callgrind.out.12345
# 5. 특정 함수만 필터
callgrind_annotate --inclusive=yes callgrind.out.12345 | head -80
출력: callgrind_annotate로 함수별 명령어 수(Ir) 확인. 상위가 병목.
Cachegrind: 캐시 미스 분석
# L1/L2 캐시 미스 시뮬레이션
valgrind --tool=cachegrind ./myapp
# 출력 예시:
# ==12345== I1 cache: 32768 B, 64 B, 8-way
# ==12345== D1 cache: 32768 B, 64 B, 8-way
# ==12345== LL cache: 262144 B, 64 B, 8-way
# ==12345==
# ==12345== D1 misses: 12,345,678 ( 10.2% of all refs)
# ==12345== LL misses: 1,234,567 ( 1.0% of all refs)
해석: D1 misses(L1 데이터 캐시 미스), LL misses(L3→DRAM)가 높으면 메모리 접근 패턴 개선 필요.
Memcheck: 메모리 누수·오류 탐지
# 메모리 누수 및 잘못된 접근 검사
valgrind --tool=memcheck --leak-check=full ./myapp
# 로그 파일로 저장
valgrind --tool=memcheck --leak-check=full --log-file=memcheck.log ./myapp
출력: definitely lost(반드시 수정), indirectly lost, possibly lost, still reachable(선택) 구분. 파일:라인으로 위치 표시.
Intel VTune: CPU 파이프라인 분석
VTune 설치 (Linux)
# Intel oneAPI (VTune 포함) - 공식 사이트에서 다운로드
# Ubuntu: sudo apt install intel-oneapi-vtune
# 설치 후: source /opt/intel/oneapi/setvars.sh
VTune 명령줄 사용법
# Hotspots 분석 (가장 흔한 시작점)
vtune -collect hotspots -result-dir vtune_result -- ./myapp
# 캐시 미스 분석
vtune -collect uarch-exploration -result-dir vtune_cache -- ./myapp
# 메모리 접근 분석
vtune -collect memory-access -result-dir vtune_mem -- ./myapp
# 결과 보기
vtune -report summary -result-dir vtune_result
vtune -report hotspots -result-dir vtune_result
VTune 출력 해석
Hotspots by CPU Time:
Function CPU Time Module
processData() 45.2% myapp
loadFile() 28.1% myapp
parseJson() 12.3% myapp
Top Micro-architectural Issues:
- L1 Data Cache Misses: 15.2% ← 메모리 접근 패턴 개선 필요
- Branch Mispredictions: 3.1% ← 분기 예측 실패
Tracy: 실시간 프로파일러
Tracy 개요
Tracy는 게임·실시간 앱용 실시간 프로파일러입니다. 코드에 존(zone)을 삽입하면, 실행 중 Tracy 클라이언트가 연결해 프레임별 지연을 타임라인으로 보여줍니다.
Tracy 설치 (CMake)
# CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG v0.10
)
FetchContent_MakeAvailable(tracy)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Tracy::TracyClient)
# 프로파일링 빌드 시
target_compile_definitions(myapp PRIVATE TRACY_ENABLE=1)
Tracy 존(Zone) 삽입
// main.cpp
#include <tracy/Tracy.hpp>
void processData(std::vector<int>& data) {
ZoneScoped; // 이 함수 전체를 하나의 존으로
for (size_t i = 0; i < data.size(); ++i) {
ZoneScopedN("ProcessItem");
data[i] = data[i] * 2 + 1;
}
}
void loadFile(const std::string& path) {
ZoneScopedN("LoadFile");
// 파일 로드...
}
int main() {
while (running) {
FrameMark; // 프레임 구분 (필수!)
{
ZoneScopedN("Update");
update();
}
{
ZoneScopedN("Physics");
physicsStep();
}
{
ZoneScopedN("Render");
render();
}
}
}
주의: FrameMark를 매 프레임 호출해야 타임라인에서 프레임 구분이 됩니다.
Tracy 실행
# 1. Tracy 프로파일러 다운로드
# https://github.com/wolfpld/tracy/releases
# 2. 애플리케이션 실행 (TRACY_ENABLE=1로 빌드된 바이너리)
./myapp
# 3. Tracy 프로파일러 실행 후 "Connect" 클릭
# 자동으로 127.0.0.1:8086에 연결
캐시 친화·비친화 코드를 프로파일링하는 예제
프로파일링 대상 C++ 프로그램
// profile_target.cpp - perf, VTune, Tracy로 분석할 대상
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
#include <iostream>
#ifdef TRACY_ENABLE
#include <tracy/Tracy.hpp>
#endif
// 의도적으로 비효율적인 함수: 캐시 미스 유발
void processDataCacheUnfriendly(std::vector<int>& data) {
#ifdef TRACY_ENABLE
ZoneScopedN("ProcessCacheUnfriendly");
#endif
// 16칸씩 건너뛰며 접근 → 캐시 라인 활용 나쁨
const size_t stride = 16;
for (size_t i = 0; i < data.size(); i += stride) {
data[i] = data[i] * 2 + 1;
}
}
// 캐시 친화적: 연속 접근
void processDataCacheFriendly(std::vector<int>& data) {
#ifdef TRACY_ENABLE
ZoneScopedN("ProcessCacheFriendly");
#endif
for (size_t i = 0; i < data.size(); ++i) {
data[i] = data[i] * 2 + 1;
}
}
// 병목 후보: O(n log n) 정렬
void sortData(std::vector<int>& data) {
#ifdef TRACY_ENABLE
ZoneScopedN("SortData");
#endif
std::sort(data.begin(), data.end());
}
// 병목 후보: 난수 생성
void fillRandom(std::vector<int>& data) {
#ifdef TRACY_ENABLE
ZoneScopedN("FillRandom");
#endif
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 1000000);
for (auto& v : data) {
v = dis(gen);
}
}
int main() {
const size_t N = 10'000'000;
std::vector<int> data(N);
{
#ifdef TRACY_ENABLE
ZoneScopedN("FillRandom");
#endif
fillRandom(data);
}
{
#ifdef TRACY_ENABLE
ZoneScopedN("SortData");
#endif
sortData(data);
}
{
#ifdef TRACY_ENABLE
ZoneScopedN("ProcessCacheUnfriendly");
#endif
processDataCacheUnfriendly(data);
}
{
#ifdef TRACY_ENABLE
ZoneScopedN("ProcessCacheFriendly");
#endif
processDataCacheFriendly(data);
}
#ifdef TRACY_ENABLE
FrameMark;
#endif
return 0;
}
컴파일 및 실행
# perf/VTune용 (디버그 심볼 포함, 최적화)
g++ -std=c++17 -O2 -g -o profile_target profile_target.cpp
# gprof용 (-pg 추가)
g++ -std=c++17 -O2 -pg -g -o profile_target_gprof profile_target.cpp
./profile_target_gprof
gprof profile_target_gprof gmon.out
# Valgrind용 (최적화 없이도 동작, -O0 권장)
g++ -std=c++17 -O0 -g -o profile_target_valgrind profile_target.cpp
valgrind --tool=callgrind ./profile_target_valgrind
valgrind --tool=cachegrind ./profile_target_valgrind
valgrind --tool=memcheck --leak-check=full ./profile_target_valgrind
# Tracy용
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -DTRACY_ENABLE=1
# perf 프로파일링
perf record -F 99 -g ./profile_target
# 화염 그래프 (FlameGraph 스크립트 필요)
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
화염 그래프 읽는 법
화염 그래프 구조
flowchart TB
subgraph Flame["화염 그래프 (가로 = CPU 시간 비율)"]
direction TB
M[main - 100%]
M --> F[fillRandom - 35%]
M --> S[sortData - 45%]
M --> P[processData - 20%]
S --> S1[std::sort - 40%]
S --> S2[비교 함수 - 5%]
P --> P1[루프 - 18%]
P --> P2[기타 - 2%]
end
읽는 법:
- 가로(너비): 해당 함수가 CPU 시간에서 차지하는 비율. 넓을수록 병목.
- 세로(스택): 아래 → 위가 호출자 → 피호출자.
main→sortData→std::sort순. - 넓은 막대: 최적화 우선순위 1순위.
화염 그래프에서 자주 보는 패턴
| 패턴 | 의미 | 대응 |
|---|---|---|
memcpy가 넓음 | 메모리 복사 병목 | 버퍼 풀, zero-copy |
malloc/free가 넓음 | 할당/해제 비용 | 객체 풀, arena |
std::sort가 넓음 | 정렬 비용 | 정렬 제거, 부분 정렬 |
pthread_mutex_lock | 락 대기 | 락 최소화, lock-free |
화염 그래프 완전한 생성 예제
git clone --depth 1 https://github.com/brendangregg/FlameGraph
export PATH="$PATH:$(pwd)/FlameGraph"
perf record -F 99 -g --call-graph dwarf,8192 ./profile_target
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
open flamegraph.svg # macOS / xdg-open (Linux)
??? 심볼, 권한 오류, 빈 화염 그래프: 프로파일링 문제 해결
perf report에서 심볼이 ???로 나옴
원인: 디버그 심볼 없이 컴파일했거나, 스택 언와인딩 실패. 해결법:
# ✅ -g 옵션으로 컴파일
g++ -std=c++17 -O2 -g -o myapp main.cpp
# ✅ perf record 시 dwarf 사용
perf record -F 99 --call-graph dwarf,8192 ./myapp
# ✅ 심볼 로드 확인
perf report -v
# "Symbols" 항목에 myapp 경로가 있어야 함
// ❌ 인라인 최적화로 함수가 사라지는 경우
// -O2에서 작은 함수는 인라인됨 → perf에 안 보일 수 있음
__attribute__((noinline)) void criticalPath() {
// 이 함수는 인라인되지 않음
}
“Permission denied” - perf 권한 오류
원인: perf_event_paranoid 설정으로 인한 권한 제한.
해결법:
# 현재 설정 확인
cat /proc/sys/kernel/perf_event_paranoid
# 3: 모든 사용자 제한, 2: 커널 프로파일 제한, 1: CPU 이벤트 제한, -1: 제한 없음
# 임시 해제 (재부팅 시 초기화)
sudo sysctl -w kernel.perf_event_paranoid=-1
# 영구 설정
echo "kernel.perf_event_paranoid = -1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
VTune “Unable to attach” 오류
원인: ptrace 권한 또는 Intel 드라이버 미설치. 해결법:
# ptrace 스코프 확인
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# VTune 드라이버 수동 로드
sudo modprobe sep
# 또는
source /opt/intel/oneapi/setvars.sh
Tracy 연결 안 됨
원인: TRACY_ENABLE 미정의, 방화벽, 포트 충돌.
해결법:
// ✅ CMake에서 정의 확인
// -DTRACY_ENABLE=1 또는
target_compile_definitions(myapp PRIVATE TRACY_ENABLE=1)
// ✅ 코드에서 확인
#ifdef TRACY_ENABLE
// ZoneScoped 등이 실제로 컴파일되는지
#endif
# 포트 8086 확인
netstat -an | grep 8086
# 방화벽 (Linux)
sudo ufw allow 8086/udp
perf stat “Events not found”
원인: AMD CPU에서 Intel 전용 이벤트 사용, 또는 권한 부족. 해결법:
# ❌ Intel 전용 이벤트 (AMD에서 실패)
perf stat -e offcore_response.demand_data_rd.llc_miss.local_dram ./myapp
# ✅ 공통 이벤트 사용
perf stat -e cycles,instructions,cache-misses ./myapp
# 사용 가능한 이벤트 목록
perf list
프로파일링 시 프로그램이 10배 이상 느려짐
원인: Valgrind 사용 중이거나, perf 샘플링 주기가 너무 높음. 해결법:
# Valgrind는 10~50배 느림 → 짧은 실행에만 사용
# perf는 샘플링이므로 1~5% 오버헤드만 있음
# 샘플링 주기 낮추기 (오버헤드 감소)
perf record -F 49 -g ./myapp # 99 → 49Hz
gprof에서 gmon.out이 생성되지 않음
원인: -pg 없이 컴파일했거나, 정상 종료되지 않음 (abort, kill).
해결법:
# ✅ -pg 옵션 필수 (링크 단계에도 필요)
g++ -std=c++17 -O2 -pg -g -o myapp main.cpp
# ✅ main에서 return 0 또는 exit(0)으로 정상 종료
# signal로 죽으면 gmon.out이 비어 있거나 없을 수 있음
# ✅ 링크된 모든 .a/.so도 -pg로 빌드된 것이 좋음
Valgrind “Invalid read/write” - 초기화되지 않은 메모리
원인: 스택/힙 변수를 초기화하지 않고 사용. 해결법:
// ❌ 초기화 없이 사용
int buffer[100];
for (int i = 0; i < 100; ++i) {
sum += buffer[i]; // Valgrind: Conditional jump on uninitialised value
}
// ✅ 0으로 초기화
int buffer[100]{};
// 또는
std::vector<int> buffer(100, 0);
Valgrind Memcheck에서 “still reachable”만 나옴
원인: 프로그램 종료 시점에 아직 해제되지 않은 할당. 누수는 아니지만 정리하면 좋음. 해결법:
"definitely lost": 반드시 수정 (메모리 누수)
"indirectly lost": 반드시 수정
"possibly lost": 수동 검토
"still reachable": 종료 시 해제 안 됨 - 글로벌/static 등. 선택적 수정
화염 그래프가 비어 있거나 “all”만 보임
원인: perf script 출력이 스택 정보 없이 나옴, 또는 dwarf 언와인딩 실패.
해결법:
# ✅ -g와 dwarf 함께 사용
perf record -F 99 -g --call-graph dwarf,8192 ./myapp
# ✅ 스택 깊이 확인
perf report --stdio
# "no symbols" 또는 스택이 짧으면 dwarf 크기 늘리기
# ✅ FlameGraph 스크립트 경로 확인
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > out.svg
도구 오버헤드와 샘플링 정확도 비교
캐시 친화적 vs 비친화적 (위 profile_target 기준)
| 함수 | 처리하는 원소 | 건드리는 캐시 라인 | perf에서 기대되는 모습 |
|---|---|---|---|
| processDataCacheFriendly | 전체 N개 | N/16개 (한 라인에 int 16개) | 라인마다 16개를 모두 쓰므로 원소당 비용이 낮고 IPC가 높음 |
| processDataCacheUnfriendly | N/16개 (16칸 간격) | N/16개 (매 접근이 새 라인) | 일은 16분의 1인데 메모리 트래픽은 같아, 원소당 비용이 훨씬 높고 IPC가 낮음 |
결론: 두 함수는 같은 수의 캐시 라인을 메모리에서 가져옵니다. 그래서 unfriendly 쪽이 연산은 16분의 1만 하는데도 실행 시간이 크게 줄지 않고, perf stat에서 명령어당 사이클(IPC의 역수)이 나빠지는 것이 보입니다. 메모리에서 가져온 캐시 라인을 얼마나 알뜰하게 쓰는지가 성능을 좌우한다는 점을 확인하는 예제입니다.
프로파일링 도구 오버헤드
| 도구 | 설정 | 오버헤드를 결정하는 것 |
|---|---|---|
| gprof -pg | - | 모든 함수 진입마다 카운터 호출이 들어가므로, 작은 함수를 자주 부르는 코드일수록 커짐 |
| perf -F 99 | 기본 | 초당 99번 인터럽트로 스택 기록. 대부분의 워크로드에서 작음 |
| perf -F 999 | 고빈도 | 샘플 수가 10배라 오버헤드도 대략 비례해 늘지만, 짧은 실행에서 통계가 안정됨 |
| VTune hotspots | 기본 | 샘플링 방식은 perf와 비슷, 하드웨어 이벤트를 많이 켤수록 증가 |
| Tracy (ZoneScoped) | 기본 | 존 하나당 타임스탬프 기록. 존을 단 함수의 호출 빈도에 비례 |
| Valgrind callgrind / cachegrind | - | 모든 명령을 가상 CPU에서 해석하므로 수십 배 느려짐(cachegrind는 캐시 시뮬레이션까지 수행) |
정확한 오버헤드는 워크로드에 따라 크게 다르므로, 같은 입력으로 도구 없이 한 번, 도구를 켜고 한 번 실행해 실행 시간을 비교해 보세요.
perf 샘플링 주기별 정확도
샘플 수 = 실행시간(초) × 주기(Hz)
예: 10초 실행, 99Hz → 990 샘플
상위 함수가 50%라면 ~495 샘플 → 통계적으로 충분
짧은 실행(<1초): 999Hz 권장
긴 실행(>10초): 99Hz로도 충분
측정 우선·기준선·한 번에 하나: 프로파일링 원칙
추측하지 말고 측정하라
❌ "이 함수가 느린 것 같아요" → 최적화 시작
✅ perf/gprof로 상위 5개 함수 확인 후, 실제 병목부터 최적화
기준선(baseline)을 먼저 확립
# 최적화 전 실행 시간 기록
perf stat -r 5 ./myapp
# 또는
time ./myapp
# 최적화 후 동일 조건으로 재측정
# 개선 여부를 수치로 확인
한 번에 하나씩 최적화
여러 함수를 동시에 수정하면:
- 어떤 변경이 효과 있었는지 알 수 없음
- 회귀 시 원인 추적 어려움
→ 한 함수 최적화 → 측정 → 다음 함수
도구별 적재적소
| 목적 | 권장 | 비권장 |
|---|---|---|
| CPU 병목 | perf + 화염 그래프 | Valgrind |
| 메모리 누수 | Valgrind Memcheck | perf |
| 캐시 효율 | Cachegrind, perf stat | gprof |
| 실시간 프레임 | Tracy | perf |
| 레거시 | gprof | - |
프로파일 빌드와 릴리스 빌드 구분
// 프로파일용: -g -O2 (디버그 심볼 + 최적화)
// 릴리스: -O3 -DNDEBUG (심볼 제거 가능)
// Tracy는 조건부 컴파일로 프로덕션에서 제거
#ifdef TRACY_ENABLE
ZoneScopedN("CriticalSection");
#endif
샘플 수가 충분한지 확인
perf: 99Hz×10초=990 샘플. 상위 10% 함수면 ~99샘플→부족할 수 있음. 30초 이상 또는 999Hz.
gprof: 1초 미만 실행은 샘플 부족. 긴 워크로드로 실행.
외부 요인 제거
# CPU 주파수 고정 (터보 부스트 영향 제거)
sudo cpupower frequency-set -g performance
# 네트워크/디스크 I/O 워크로드는 여러 번 반복해 평균
프로덕션 샘플링·주기적 프로파일링·조건부 Tracy
perf로 프로덕션 샘플링
flowchart LR
A[프로덕션 서버] --> B[perf record -F 49]
B --> C[30초 수집]
C --> D[perf.data 저장]
D --> E[개발 PC로 전송]
E --> F[perf report / 화염 그래프]
# 프로덕션에서 30초간 샘플링 (오버헤드 최소)
perf record -F 49 -g -o /tmp/perf.data -- sleep 30 &
# 실제로는: perf record -F 49 -g -p $(pgrep myapp) -o /tmp/perf.data -- sleep 30
# 프로세스 ID로 연결
perf record -F 49 -g -p 12345 -o /tmp/perf.data -- sleep 30
# 결과 파일만 복사하여 로컬에서 분석
scp server:/tmp/perf.data .
perf report -i perf.data
주기적 프로파일링 (cron)
#!/bin/bash
# /opt/scripts/profile_production.sh
OUT_DIR="/var/log/profiles"
mkdir -p "$OUT_DIR"
DATE=$(date +%Y%m%d_%H%M%S)
PID=$(pgrep -f myapp | head -1)
if [ -n "$PID" ]; then
perf record -F 49 -g -p "$PID" -o "$OUT_DIR/perf_$DATE.data" -- sleep 60
# 60초 후 자동 종료
fi
# crontab: 매일 새벽 3시에 1분간 프로파일
0 3 * * * /opt/scripts/profile_production.sh
Tracy 조건부 컴파일
// 프로덕션 빌드에서는 Tracy 비활성화 (오버헤드 제거)
#ifdef TRACY_ENABLE
#define PROFILE_SCOPE(name) ZoneScopedN(name)
#define PROFILE_FRAME() FrameMark
#else
#define PROFILE_SCOPE(name) ((void)0)
#define PROFILE_FRAME() ((void)0)
#endif
void update() {
PROFILE_SCOPE("Update");
// ...
}
벤치마크 기준선 확립
// benchmark_baseline.cpp
#include <chrono>
#include <iostream>
int main() {
const int iterations = 100;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
runWorkload();
}
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Baseline: " << (ms / double(iterations)) << " ms/iter\n";
return 0;
}
Valgrind는 짧은 워크로드로
valgrind --tool=memcheck --leak-check=full ./run_tests
# CI: --error-exitcode=1 로 누수 시 실패
프로덕션 체크리스트:
- perf 샘플링 주기 49
99Hz (오버헤드 15%) - Tracy는 개발/스테이징에서만 TRACY_ENABLE=1
- VTune은 스테이징에서만 사용 (오버헤드 10%+)
- Valgrind는 프로덕션 사용 금지 (10~50배 느림)
- 프로파일 데이터는 디스크 용량 모니터링 (perf.data 수백 MB 가능)
도구별 사용 전 점검 항목
perf 사용 체크리스트
-
-g옵션으로 컴파일 (디버그 심볼) -
perf record -F 99 -g또는--call-graph dwarf -
perf_event_paranoid설정 확인 - FlameGraph 스크립트로 화염 그래프 생성
-
perf stat로 IPC, 캐시 미스율 확인
gprof 사용 체크리스트
-
-pg -g옵션으로 컴파일 - 정상 종료 (return/exit)로 gmon.out 생성 확인
-
gprof -p플랫 프로파일,gprof -q호출 그래프 - 인라인/공유 라이브러리 한계 인지
Valgrind 사용 체크리스트
-
-g디버그 심볼 (에러 위치 표시) - Callgrind:
--tool=callgrind, KCachegrind로 시각화 - Cachegrind: L1/L2 캐시 미스 확인
- Memcheck:
--leak-check=full메모리 누수 - 짧은 실행만 (10~50배 느림)
VTune 사용 체크리스트
- Intel CPU 환경 확인
- oneAPI/VTune 설치 및
setvars.sh실행 - hotspots → microarchitecture → memory-access 순서로 분석
- ptrace_scope 설정
Tracy 사용 체크리스트
- CMake에 Tracy FetchContent 추가
-
ZoneScoped/ZoneScopedN/FrameMark삽입 -
TRACY_ENABLE=1로 빌드 - Tracy 프로파일러 실행 후 Connect
- 프로덕션에서는
TRACY_ENABLE=0
프로파일링 워크플로우 순서
- 1단계: perf로 빠른 병목 탐색
- 2단계: 화염 그래프로 시각화
- 3단계: 메모리 누수 의심 시 Valgrind Memcheck
- 4단계: 캐시 효율 의심 시 Valgrind Cachegrind 또는 perf stat
- 5단계: 캐시/파이프라인 심층 분석 시 VTune
- 6단계: 실시간 프레임 분석 필요 시 Tracy
- 7단계: 최적화 후 재측정으로 검증
도구별 특징 요약
| 항목 | 설명 |
|---|---|
| perf | Linux 표준, 화염 그래프, 낮은 오버헤드, 프로덕션 샘플링 가능 |
| gprof | 호출 그래프·플랫 프로파일, -pg 컴파일, 레거시 환경 |
| Valgrind | Callgrind(CPU), Cachegrind(캐시), Memcheck(메모리), 10~50배 느림 |
| VTune | Intel CPU 심층 분석, 캐시/파이프라인/메모리 대역폭 |
| Tracy | 실시간 프레임 프로파일링, 게임/실시간 앱 |
| 화염 그래프 | 가로=CPU 비율, 세로=호출 스택, 넓은 막대=병목 |
| 프로덕션 | perf -F 49~99, Tracy 비활성화, 주기적 샘플링 |
핵심 원칙:
- 추측하지 말고 측정하라.
- perf로 먼저 병목 탐색, 필요 시 VTune/Tracy.
- 화염 그래프로 시각화하여 넓은 막대부터 최적화.
- 프로덕션에서는 낮은 샘플링 주기와 조건부 Tracy.
자주 묻는 질문 (FAQ)
Q. perf report에 함수 이름 대신 주소나 unknown만 보이는 이유는 무엇인가요?
A. 대부분 바이너리에 디버그 심볼이 없거나 strip되어 있어서 perf가 주소를 함수 이름으로 바꾸지 못하는 경우입니다. 프로파일링용 빌드는 최적화를 유지한 채 -g를 함께 넣고, 호출 스택이 끊겨 보인다면 -fno-omit-frame-pointer로 빌드하거나 perf record —call-graph dwarf를 사용합니다. 커널 심볼까지 unknown으로 나온다면 kernel.perf_event_paranoid나 kptr_restrict 같은 권한 설정도 함께 확인해야 합니다.
Q. perf, gprof, Valgrind, VTune, Tracy 중 뭘 써야 하나요?
A. Linux 서버 CPU 병목: perf(무료·가벼움). 메모리 누수: Valgrind Memcheck. 캐시 시뮬레이션: Valgrind Cachegrind. Intel CPU 심층 분석: VTune. 게임/실시간 앱: Tracy. 레거시/간단한 프로파일: gprof. 글 내 도구 선택 다이어그램을 참고하세요.
Q. 프로덕션에서 프로파일링해도 되나요?
A. perf는 오버헤드 1~5%로 프로덕션 샘플링 가능합니다. VTune·Tracy는 개발/스테이징 환경을 권장합니다. 프로덕션 패턴 섹션을 참고하세요.
perf·gprof·Valgrind·VTune·Tracy로 병목을 찾으며, 화염 그래프로 시각화하며, 메모리·캐시를 분석하며, 프로덕션에서 안전하게 샘플링하는 방법을 마스터할 수 있습니다.
같이 보면 좋은 글
- C++ SIMD 최적화: SSE·AVX2·AVX-512·NEON 인트린직, 자동 벡터화, C++26 std::simd
- C++ 캐시 최적화 실전 | 캐시 친화적 구조·프리페치·False Sharing·AoS vs SoA 가이드
- C++17 스레드 풀 만들기
- C++ 프로파일링 | ‘어디가 느린지 모르겠어요’ perf·gprof로 병목 찾기
- C++ Benchmarking
- C++ 스택 vs 힙 | 재귀에서 프로그램이 죽는 이유와 스택 오버플로우 사례
- C++ 메모리 누수: 흔한 5가지 패턴과 Valgrind·ASan으로 찾는 방법
- Valgrind Memcheck로 C++ 메모리 누수 찾아내기
- C++ Advanced Profiling Guide | perf· gprof (English)