C++ 고급 프로파일링 완벽 가이드 | perf·gprof
이 글의 핵심
C++ 멀티스레드 게임 서버가 60% CPU를 쓰는데 어디가 병목인지 모를 때. perf·gprof·Valgrind(Callgrind·Cachegrind·Memcheck)·VTune·Tracy, 화염 그래프, 캐시 미스 분석까지 실전 코드와 벤치마크로 마스터합니다.
들어가며: “멀티스레드 서버가 60% CPU를 쓰는데 어디가 병목인지 모르겠어요”
문제 시나리오
실제 겪는 상황:
- 게임 서버가 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)
목차
- 문제 시나리오와 도구 선택
- perf 심화: 화염 그래프와 캐시 프로파일링
- gprof: 호출 그래프 플랫 프로파일
- Valgrind: Callgrind·Cachegrind·Memcheck
- Intel VTune: CPU 파이프라인 분석
- Tracy: 실시간 프로파일러
- 완전한 벤치마크 예제
- 화염 그래프 읽는 법
- 자주 발생하는 문제와 해결법
- 성능 벤치마크 비교
- 프로파일링 모범 사례
- 프로덕션 프로파일링 패턴
- 체크리스트
1. 문제 시나리오와 도구 선택
언제 어떤 도구를 쓸까?
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: 프레임별 지연 타임라인
2. 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'로 심볼별 정렬
3. 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 조절 가능 |
4. 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(선택) 구분. 파일:라인으로 위치 표시.
5. 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% ← 분기 예측 실패
6. 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에 연결
7. 완전한 벤치마크 예제
프로파일링 대상 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
8. 화염 그래프 읽는 법
화염 그래프 구조
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)
9. 자주 발생하는 문제와 해결법
문제 1: 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() {
// 이 함수는 인라인되지 않음
}
문제 2: “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
문제 3: 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
문제 4: 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
문제 5: 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
문제 6: 프로파일링 시 프로그램이 10배 이상 느려짐
원인: Valgrind 사용 중이거나, perf 샘플링 주기가 너무 높음.
해결법:
# Valgrind는 10~50배 느림 → 짧은 실행에만 사용
# perf는 샘플링이므로 1~5% 오버헤드만 있음
# 샘플링 주기 낮추기 (오버헤드 감소)
perf record -F 49 -g ./myapp # 99 → 49Hz
문제 7: 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로 빌드된 것이 좋음
문제 8: 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);
문제 9: Valgrind Memcheck에서 “still reachable”만 나옴
원인: 프로그램 종료 시점에 아직 해제되지 않은 할당. 누수는 아니지만 정리하면 좋음.
해결법:
"definitely lost": 반드시 수정 (메모리 누수)
"indirectly lost": 반드시 수정
"possibly lost": 수동 검토
"still reachable": 종료 시 해제 안 됨 - 글로벌/static 등. 선택적 수정
문제 10: 화염 그래프가 비어 있거나 “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
10. 성능 벤치마크 비교
캐시 친화적 vs 비친화적 (위 profile_target 기준)
| 함수 | 실행 시간 (N=1000만) | 캐시 미스율 | IPC |
|---|---|---|---|
| processDataCacheFriendly | 12 ms | 2.1% | 2.8 |
| processDataCacheUnfriendly | 89 ms | 18.3% | 0.4 |
결론: 동일 연산이라도 메모리 접근 패턴에 따라 7배 이상 차이.
프로파일링 도구 오버헤드
| 도구 | 설정 | 오버헤드 | 실행 시간 배율 |
|---|---|---|---|
| 없음 | - | 0% | 1.00x |
| gprof -pg | - | 5~15% | 1.05~1.15x |
| perf -F 99 | 기본 | ~2% | 1.02x |
| perf -F 999 | 고빈도 | ~8% | 1.08x |
| VTune hotspots | 기본 | ~10% | 1.10x |
| Tracy (ZoneScoped) | 기본 | ~0.5% | 1.005x |
| Valgrind callgrind | - | 1000~5000% | 10~50x |
| Valgrind cachegrind | - | 500~2000% | 5~20x |
perf 샘플링 주기별 정확도
샘플 수 = 실행시간(초) × 주기(Hz)
예: 10초 실행, 99Hz → 990 샘플
상위 함수가 50%라면 ~495 샘플 → 통계적으로 충분
짧은 실행(<1초): 999Hz 권장
긴 실행(>10초): 99Hz로도 충분
11. 프로파일링 모범 사례
원칙 1: 추측하지 말고 측정하라
❌ "이 함수가 느린 것 같아요" → 최적화 시작
✅ perf/gprof로 상위 5개 함수 확인 후, 실제 병목부터 최적화
원칙 2: 기준선(baseline)을 먼저 확립
# 최적화 전 실행 시간 기록
perf stat -r 5 ./myapp
# 또는
time ./myapp
# 최적화 후 동일 조건으로 재측정
# 개선 여부를 수치로 확인
원칙 3: 한 번에 하나씩 최적화
여러 함수를 동시에 수정하면:
- 어떤 변경이 효과 있었는지 알 수 없음
- 회귀 시 원인 추적 어려움
→ 한 함수 최적화 → 측정 → 다음 함수
원칙 4: 도구별 적재적소
| 목적 | 권장 | 비권장 |
|---|---|---|
| CPU 병목 | perf + 화염 그래프 | Valgrind |
| 메모리 누수 | Valgrind Memcheck | perf |
| 캐시 효율 | Cachegrind, perf stat | gprof |
| 실시간 프레임 | Tracy | perf |
| 레거시 | gprof | - |
원칙 5: 프로파일 빌드와 릴리스 빌드 구분
// 프로파일용: -g -O2 (디버그 심볼 + 최적화)
// 릴리스: -O3 -DNDEBUG (심볼 제거 가능)
// Tracy는 조건부 컴파일로 프로덕션에서 제거
#ifdef TRACY_ENABLE
ZoneScopedN("CriticalSection");
#endif
원칙 6: 샘플 수가 충분한지 확인
perf: 99Hz×10초=990 샘플. 상위 10% 함수면 ~99샘플→부족할 수 있음. 30초 이상 또는 999Hz.
gprof: 1초 미만 실행은 샘플 부족. 긴 워크로드로 실행.
원칙 7: 외부 요인 제거
# CPU 주파수 고정 (터보 부스트 영향 제거)
sudo cpupower frequency-set -g performance
# 네트워크/디스크 I/O 워크로드는 여러 번 반복해 평균
12. 프로덕션 프로파일링 패턴
패턴 1: 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
패턴 2: 주기적 프로파일링 (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
패턴 3: 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");
// ...
}
패턴 4: 벤치마크 기준선 확립
// 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;
}
패턴 5: 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 가능)
13. 체크리스트
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. 이 내용을 실무에서 언제 쓰나요?
A. 병목 지점 파악, CPU/메모리 사용량 분석, 캐시 미스 추적, 멀티스레드 성능 분석 등 성능 최적화의 첫 단계입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
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는 개발/스테이징 환경을 권장합니다. 프로덕션 패턴 섹션을 참고하세요.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. Brendan Gregg 블로그(화염 그래프), Intel VTune 문서, Tracy GitHub를 참고하세요.
한 줄 요약: perf·gprof·Valgrind·VTune·Tracy로 병목을 찾고, 화염 그래프로 시각화하며, 메모리·캐시를 분석하고, 프로덕션에서 안전하게 샘플링하는 방법을 마스터할 수 있습니다.
관련 글
- C++ SIMD 최적화 실전 | SSE·AVX2·NEON 인트린직으로 4배 빠르게 [#51-2]
- C++ 캐시 최적화 실전 | 캐시 친화적 구조·프리페치·False Sharing·AoS vs SoA 가이드
- C++ 스레드 풀 완벽 가이드 | 작업 큐·병렬 처리·성능 벤치마크 [#51-3]
- C++ 프로파일링 |
- C++ Benchmarking |
- C++ 스택 vs 힙 완벽 가이드 | 재귀 크래시, 메모리 레이아웃, RAII·스마트 포인터 실전 패턴
- C++ 메모리 누수 | 서버 다운시킨 실제 사례와 Valgrind로 찾는 5가지 패턴
- C++ Valgrind 완벽 가이드 | Memcheck·누수 탐지