C++ 고급 프로파일링 완벽 가이드 | perf·gprof

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)


목차

  1. 문제 시나리오와 도구 선택
  2. perf 심화: 화염 그래프와 캐시 프로파일링
  3. gprof: 호출 그래프 플랫 프로파일
  4. Valgrind: Callgrind·Cachegrind·Memcheck
  5. Intel VTune: CPU 파이프라인 분석
  6. Tracy: 실시간 프로파일러
  7. 완전한 벤치마크 예제
  8. 화염 그래프 읽는 법
  9. 자주 발생하는 문제와 해결법
  10. 성능 벤치마크 비교
  11. 프로파일링 모범 사례
  12. 프로덕션 프로파일링 패턴
  13. 체크리스트

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

도구별 특징 비교

도구오버헤드프로덕션강점약점
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: 프레임별 지연 타임라인

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 도구 비교

도구용도출력
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(선택) 구분. 파일:라인으로 위치 표시.


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 시간에서 차지하는 비율. 넓을수록 병목.
  • 세로(스택): 아래 → 위가 호출자 → 피호출자. mainsortDatastd::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
processDataCacheFriendly12 ms2.1%2.8
processDataCacheUnfriendly89 ms18.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 Memcheckperf
캐시 효율Cachegrind, perf statgprof
실시간 프레임Tracyperf
레거시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 샘플링 주기 4999Hz (오버헤드 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단계: 최적화 후 재측정으로 검증

정리

항목설명
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. 이 내용을 실무에서 언제 쓰나요?

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·누수 탐지
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3