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[실시간 타임라인]

도구별 특징 비교

도구오버헤드프로덕션강점약점
perf1~5%✅ 가능무료, Linux 표준, 화염 그래프AMD에서 일부 이벤트 제한
gprof5~15%△ 가능호출 그래프, 설치 간단샘플링 부정확, 인라인 무시
Valgrind10~50배❌ 불가메모리 누수, 캐시 시뮬레이션매우 느림, 짧은 실행만
VTune5~15%△ 스테이징캐시/파이프라인 심층 분석Intel 전용, 상용
Tracy0.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 도구 비교

도구용도출력
CallgrindCPU 프로파일링, 호출 횟수callgrind.out.*, KCachegrind로 시각화
CachegrindL1/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가 높음
processDataCacheUnfriendlyN/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 Memcheckperf
캐시 효율Cachegrind, perf statgprof
실시간 프레임Tracyperf
레거시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 샘플링 주기 4999Hz (오버헤드 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단계: 최적화 후 재측정으로 검증

도구별 특징 요약

항목설명
perfLinux 표준, 화염 그래프, 낮은 오버헤드, 프로덕션 샘플링 가능
gprof호출 그래프·플랫 프로파일, -pg 컴파일, 레거시 환경
ValgrindCallgrind(CPU), Cachegrind(캐시), Memcheck(메모리), 10~50배 느림
VTuneIntel CPU 심층 분석, 캐시/파이프라인/메모리 대역폭
Tracy실시간 프레임 프로파일링, 게임/실시간 앱
화염 그래프가로=CPU 비율, 세로=호출 스택, 넓은 막대=병목
프로덕션perf -F 49~99, Tracy 비활성화, 주기적 샘플링

핵심 원칙:

  1. 추측하지 말고 측정하라.
  2. perf로 먼저 병목 탐색, 필요 시 VTune/Tracy.
  3. 화염 그래프로 시각화하여 넓은 막대부터 최적화.
  4. 프로덕션에서는 낮은 샘플링 주기와 조건부 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로 병목을 찾으며, 화염 그래프로 시각화하며, 메모리·캐시를 분석하며, 프로덕션에서 안전하게 샘플링하는 방법을 마스터할 수 있습니다.


같이 보면 좋은 글