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

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

이 글의 핵심

C++ 멀티스레드 게임 서버가 60% CPU를 쓰는데 어디가 병목인지 모를 때. perf·gprof·Valgrind(Callgrind·Cachegrind·Memcheck)·VTune·Tracy, 화염 그래프, 캐시 미스 분석까지 실전 코드와 벤치마크로 마스터합니다.

들어가며: “멀티스레드

일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. 서버가 60% CPU를 쓰는데 어디가 병목인지 모르겠어요”

문제 시나리오

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

실제 겪는 상황:
- 게임 서버가 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)

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

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

1. 문제 시나리오와 도구 선택

언제 어떤 도구를 쓸까?

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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%△ 선택적실시간, 프레임 단위코드 수정 필요

프로파일링 워크플로우

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 고급 옵션

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 샘플링 주기: 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 미스)

화염 그래프 생성 (완전한 예제)

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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: 하드웨어 이벤트 분석

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 기본 통계
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

출력 해석: 아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

 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: 소스 라인별 분석

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 특정 함수의 어셈블리 + 소스 라인별 샘플 수
perf annotate -s processData
# perf record 후
perf report
# 'a' 키로 annotate, 's'로 심볼별 정렬

3. gprof: 호출 그래프 플랫 프로파일

gprof 개요

gprof는 GCC와 함께 제공되는 플랫 프로파일러입니다. -pg 옵션으로 컴파일하면 실행 시 gmon.out을 생성하고, 이를 분석해 함수별 CPU 시간 비율호출 그래프를 보여줍니다. 레거시 환경이나 perf가 없는 시스템에서 유용하지만, 인라인 함수·공유 라이브러리에서 부정확할 수 있습니다.

gprof 완전한 사용법

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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 출력 해석

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 호출 그래프 예시

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 프로파일링

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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: 캐시 미스 분석

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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: 메모리 누수·오류 탐지

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 메모리 누수 및 잘못된 접근 검사
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 명령줄 사용법

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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 출력 해석

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)

아래 코드는 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) 삽입

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 실행

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 1. Tracy 프로파일러 다운로드
# https://github.com/wolfpld/tracy/releases
# 2. 애플리케이션 실행 (TRACY_ENABLE=1로 빌드된 바이너리)
./myapp
# 3. Tracy 프로파일러 실행 후 "Connect" 클릭
# 자동으로 127.0.0.1:8086에 연결

7. 완전한 벤치마크 예제

프로파일링 대상 C++ 프로그램

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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;
}

컴파일 및 실행

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 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. 화염 그래프 읽는 법

화염 그래프 구조

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

화염 그래프 완전한 생성 예제

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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에서 심볼이 ???로 나옴

원인: 디버그 심볼 없이 컴파일했거나, 스택 언와인딩 실패. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ✅ -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 경로가 있어야 함

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 인라인 최적화로 함수가 사라지는 경우
// -O2에서 작은 함수는 인라인됨 → perf에 안 보일 수 있음
__attribute__((noinline)) void criticalPath() {
    // 이 함수는 인라인되지 않음
}

문제 2: “Permission denied” - perf 권한 오류

원인: perf_event_paranoid 설정으로 인한 권한 제한. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 현재 설정 확인
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 드라이버 미설치. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 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 미정의, 방화벽, 포트 충돌. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ CMake에서 정의 확인
// -DTRACY_ENABLE=1 또는
target_compile_definitions(myapp PRIVATE TRACY_ENABLE=1)
// ✅ 코드에서 확인
#ifdef TRACY_ENABLE
    // ZoneScoped 등이 실제로 컴파일되는지
#endif

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 포트 8086 확인
netstat -an | grep 8086
# 방화벽 (Linux)
sudo ufw allow 8086/udp

문제 5: perf stat “Events not found”

원인: AMD CPU에서 Intel 전용 이벤트 사용, 또는 권한 부족. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ❌ 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 샘플링 주기가 너무 높음. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# Valgrind는 10~50배 느림 → 짧은 실행에만 사용
# perf는 샘플링이므로 1~5% 오버헤드만 있음
# 샘플링 주기 낮추기 (오버헤드 감소)
perf record -F 49 -g ./myapp   # 99 → 49Hz

문제 7: gprof에서 gmon.out이 생성되지 않음

원인: -pg 없이 컴파일했거나, 정상 종료되지 않음 (abort, kill). 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ✅ -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” - 초기화되지 않은 메모리

원인: 스택/힙 변수를 초기화하지 않고 사용. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 초기화 없이 사용
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”만 나옴

원인: 프로그램 종료 시점에 아직 해제되지 않은 할당. 누수는 아니지만 정리하면 좋음. 해결법: 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

"definitely lost": 반드시 수정 (메모리 누수)
"indirectly lost": 반드시 수정
"possibly lost": 수동 검토
"still reachable": 종료 시 해제 안 됨 - 글로벌/static 등. 선택적 수정

문제 10: 화염 그래프가 비어 있거나 “all”만 보임

원인: perf script 출력이 스택 정보 없이 나옴, 또는 dwarf 언와인딩 실패. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ✅ -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 샘플링 주기별 정확도

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

샘플 수 = 실행시간(초) × 주기(Hz)
예: 10초 실행, 99Hz → 990 샘플
    상위 함수가 50%라면 ~495 샘플 → 통계적으로 충분
짧은 실행(<1초): 999Hz 권장
긴 실행(>10초): 99Hz로도 충분

11. 프로파일링 모범 사례

원칙 1: 추측하지 말고 측정하라

❌ "이 함수가 느린 것 같아요" → 최적화 시작
✅ perf/gprof로 상위 5개 함수 확인 후, 실제 병목부터 최적화

원칙 2: 기준선(baseline)을 먼저 확립

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 최적화 전 실행 시간 기록
perf stat -r 5 ./myapp
# 또는
time ./myapp
# 최적화 후 동일 조건으로 재측정
# 개선 여부를 수치로 확인

원칙 3: 한 번에 하나씩 최적화

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

여러 함수를 동시에 수정하면:
- 어떤 변경이 효과 있었는지 알 수 없음
- 회귀 시 원인 추적 어려움
→ 한 함수 최적화 → 측정 → 다음 함수

원칙 4: 도구별 적재적소

목적권장비권장
CPU 병목perf + 화염 그래프Valgrind
메모리 누수Valgrind Memcheckperf
캐시 효율Cachegrind, perf statgprof
실시간 프레임Tracyperf
레거시gprof-

원칙 5: 프로파일 빌드와 릴리스 빌드 구분

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 프로파일용: -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로 프로덕션 샘플링

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart LR
    A[프로덕션 서버] --> B[perf record -F 49]
    B --> C[30초 수집]
    C --> D[perf.data 저장]
    D --> E[개발 PC로 전송]
    E --> F[perf report / 화염 그래프]

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 프로덕션에서 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)

아래 코드는 bash를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#!/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 조건부 컴파일

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 프로덕션 빌드에서는 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: 벤치마크 기준선 확립

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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로 병목을 찾고, 화염 그래프로 시각화하며, 메모리·캐시를 분석하고, 프로덕션에서 안전하게 샘플링하는 방법을 마스터할 수 있습니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3