C++ 프로파일링 | "어디가 느린지 모르겠어요" perf·gprof로 병목 찾기

C++ 프로파일링 | "어디가 느린지 모르겠어요" perf·gprof로 병목 찾기

이 글의 핵심

C++ 프로파일링에 대한 실전 가이드입니다.

들어가며: “어디가 느린지 모르겠어요”

실무에서 겪는 문제 시나리오

실제 겪는 상황:
- "이 함수가 느릴 것 같아서" 3일 최적화했는데, 실제 병목은 파일 I/O였음
- perf report를 봐도 심볼이 ???로 나와서 분석 불가
- gprof로 프로파일했는데 gmon.out이 생성되지 않음
- Valgrind를 돌리니 30배 느려져서 실용성이 없다고 느낌
- API 서버가 CPU 100%인데 어느 핸들러가 문제인지 모름
- 24시간 실행 후 메모리가 2GB→8GB로 증가 (누수 의심)
- O(n) 알고리즘인데 n이 커지면 선형보다 훨씬 느림 (캐시 의심)

이런 상황에서 추측 대신 측정이 핵심입니다. 프로파일러로 병목을 찾고, 화염 그래프로 시각화하고, 상위 20% 함수부터 최적화하면 시간 대비 효과가 큽니다.

추측으로 최적화하다가 시간만 낭비했다

프로그램이 느려서 추측으로 최적화를 시도했습니다. 하지만 실제 병목(bottleneck—전체 성능을 제한하는 가장 느린 지점)은 다른 곳이었습니다.

잘못된 접근:

// "이 함수가 느릴 것 같아서" 최적화
void processData(std::vector<int>& data) {
    // 복잡한 최적화 시도...
}

// 실제로는 이 함수가 병목이었음
void loadData() {
    // 파일 I/O가 느림
}

프로파일링 후:

  • processData: 전체 시간의 5%
  • loadData: 전체 시간의 80% ← 진짜 병목

교훈:

  • 추측하지 말고 측정하라
  • 프로파일러로 병목 찾기
  • 가장 느린 부분부터 최적화

프로파일링(실행 중 어느 함수가 시간·메모리를 얼마나 쓰는지 측정하는 것) 없이 “이 부분이 느릴 것 같다”고 손대면, 실제로는 I/O나 다른 모듈이 병목인 경우가 많습니다. CPU 샘플링(perf 등)이나 인스트루멘테이션으로 “어느 함수가 시간을 많이 쓰는지”를 먼저 확인한 뒤, 상위 몇 %부터 최적화하는 것이 시간 대비 효과가 큽니다.

프로파일링의 전체 흐름

flowchart TD
    A[프로그램 느림] --> B[측정 없이 추측]
    B --> C{병목 맞추기}
    C -->|실패| D[시간 낭비]
    A --> E[프로파일링 실행]
    E --> F[병목 지점 확인]
    F --> G[상위 20% 구간 최적화]
    G --> H[재측정]
    H --> I{목표 달성?}
    I -->|아니오| E
    I -->|예| J[성능 개선 완료]

이 글을 읽으면:

  • 프로파일링 도구를 사용할 수 있습니다.
  • 병목 지점을 정확히 찾을 수 있습니다.
  • 성능을 정량적으로 측정할 수 있습니다.
  • 실전에서 효과적으로 최적화할 수 있습니다.

목차

  1. 프로파일링이란
  2. 시간 측정 기초
  3. 프로파일링 도구
  4. 완전한 프로파일링 예제
  5. 병목 지점 분석
  6. 실전 최적화 프로세스
  7. 자주 발생하는 문제
  8. 프로파일링 모범 사례
  9. 프로덕션 프로파일링 패턴
  10. 체크리스트

1. 프로파일링이란

성능 측정의 필요성

"추측하지 말고 측정하라"
- 직관은 자주 틀림
- 병목은 예상 밖의 곳에 있음
- 측정 없는 최적화는 시간 낭비

프로파일링 종류

1. CPU 프로파일링

  • 어떤 함수가 CPU를 많이 쓰는지
  • 함수 호출 횟수와 시간

2. 메모리 프로파일링

  • 메모리 사용량
  • 할당/해제 횟수
  • 메모리 누수

3. 캐시 프로파일링

  • 캐시 미스 횟수
  • 메모리 접근 패턴

프로파일링 종류별 비교

flowchart LR
    subgraph CPU["CPU 프로파일링"]
        C1[perf]
        C2[gprof]
        C3[VS Profiler]
    end
    subgraph MEM["메모리 프로파일링"]
        M1[Valgrind Memcheck]
        M2[AddressSanitizer]
    end
    subgraph CACHE["캐시 프로파일링"]
        K1[Valgrind Cachegrind]
        K2[perf stat]
    end

2. 시간 측정 기초

std::chrono로 측정

C++11부터 제공하는 **std::chrono**로 구간 시간을 잴 수 있습니다. high_resolution_clock::now()로 시작·종료 시점을 받고, end - start로 duration을 구한 뒤 duration_cast<std::chrono::milliseconds>로 원하는 단위로 변환합니다. 이렇게 하면 특정 함수나 블록이 실제로 몇 ms 걸리는지 숫자로 확인할 수 있어, “느리다”는 감이 아니라 데이터로 병목를 찾을 수 있습니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o profile_time profile_time.cpp && ./profile_time
#include <chrono>
#include <iostream>

void slowFunction() {
    // 무거운 작업...
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    slowFunction();
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Time: " << duration.count() << " ms\n";
    return 0;
}

실행 결과: Time: N ms 형태로 출력됩니다 (환경에 따라 N 값은 다름).

코드 상세 설명:

  • high_resolution_clock: 시스템에서 사용 가능한 가장 정밀한 시계
  • now(): 현재 시점을 time_point로 반환
  • duration_cast: 나노초 단위 duration을 밀리초로 변환
  • count(): 해당 단위의 정수 값 반환

측정 헬퍼 클래스

생성자에서 시각을 기록하고 소멸자에서 경과 시간을 출력하는 RAII 스타일 타이머입니다. { Timer t("slowFunction"); slowFunction(); }처럼 스코프를 두면, 블록을 빠져나갈 때 자동으로 소멸자가 호출되어 해당 구간 시간이 출력됩니다. 예외가 나거나 early return이 있어도 소멸자는 호출되므로, 수동으로 end 시간을 찍는 것보다 누락이 적습니다.

class Timer {
    std::chrono::high_resolution_clock::time_point start;
    const char* name;

public:
    Timer(const char* n) : name(n) {
        start = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << name << ": " << duration.count() << " us\n";
    }
};

void processData() {
    Timer timer("processData");
    // 작업...
}  // 소멸자에서 자동 출력

주의점: Timer 객체를 스코프 밖으로 두면 측정 구간이 의도와 다를 수 있습니다. { }로 블록을 명확히 구분하세요.

여러 구간 측정

class Profiler {
    std::map<std::string, long long> timings;
    std::chrono::high_resolution_clock::time_point start;

public:
    void startTimer() {
        start = std::chrono::high_resolution_clock::now();
    }

    void record(const std::string& name) {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        timings[name] += duration.count();
        start = end;
    }

    void report() {
        for (const auto& [name, time] : timings) {
            std::cout << name << ": " << time << " us\n";
        }
    }
};

int main() {
    Profiler prof;

    prof.startTimer();
    loadData();
    prof.record("loadData");

    processData();
    prof.record("processData");

    saveData();
    prof.record("saveData");

    prof.report();
}

활용: record() 호출 시점마다 “이전 구간”의 누적 시간이 기록됩니다. start = end로 다음 구간 시작을 갱신하므로, 여러 번 반복하면 전체 합계를 볼 수 있습니다.


3. 프로파일링 도구

도구 선택 가이드

flowchart TD
    A[프로파일링 필요] --> B{플랫폼?}
    B -->|Linux| C[perf]
    B -->|Linux/Mac| D[gprof]
    B -->|Linux/Mac| E[Valgrind]
    B -->|Windows| F[VS Profiler]
    C --> G[CPU 샘플링]
    D --> H[인스트루멘테이션]
    E --> I[메모리/캐시]
    F --> C

perf (Linux)

Linux의 표준 프로파일링 도구입니다. 샘플링 방식으로 CPU가 주기적으로 실행 중인 함수를 기록해, 오버헤드가 적어 프로덕션 환경에서도 사용 가능합니다.

# 프로그램 실행하며 프로파일링
perf record ./myapp

# 결과 보기
perf report

# 함수별 시간
perf stat ./myapp

출력 예시:

  50.23%  myapp  [.] processData
  30.45%  myapp  [.] loadFile
  15.32%  myapp  [.] parseJson

perf report 상세 사용법:

# 호출 그래프 포함 (함수 호출 관계)
perf record -g ./myapp

# 결과를 텍스트로 출력
perf report --stdio

# 특정 함수만 필터
perf report --symbol-filter=processData

perf stat 출력 해석:

 Performance counter stats for './myapp':

          1,234.56 msec task-clock
                42      context-switches
                 0      cpu-migrations
               128      page-faults
     3,456,789,012      cycles
     2,345,678,901      instructions
  • task-clock: CPU 사용 시간 (ms)
  • context-switches: 컨텍스트 스위칭 횟수
  • page-faults: 페이지 폴트 횟수
  • cycles: CPU 사이클
  • instructions: 실행된 명령어 수

IPC (Instructions Per Cycle): instructions / cycles가 1에 가까우면 CPU가 효율적으로 동작합니다. 0.5 이하면 메모리 대기나 분기 예측 실패 등이 의심됩니다.

gprof (GNU Profiler)

컴파일 시 -pg 플래그로 코드에 프로파일링 코드를 삽입합니다. 실행 시 gmon.out 파일이 생성되고, 이를 분석해 함수별 호출 횟수와 시간을 보여줍니다.

# -pg 플래그로 컴파일
g++ -pg -O2 main.cpp -o myapp

# 실행 (gmon.out 생성)
./myapp

# 프로파일 보기
gprof myapp gmon.out

gprof 출력 예시:

  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 80.0      0.80     0.80        1   800.00   800.00  loadFile
 15.0      0.95     0.15      100     1.50     1.50  processData
  5.0      1.00     0.05        1    50.00    50.00  saveResult

주의: -pg-O2를 함께 쓰면 인라인 최적화로 인해 일부 함수가 합쳐져 보일 수 있습니다. 정확한 호출 관계를 보려면 -O0 또는 -O1로 테스트할 수 있습니다.

Valgrind Callgrind

시뮬레이션 방식으로 CPU 명령어를 단계별 실행합니다. 정확한 호출 관계와 캐시 정보를 얻을 수 있지만, 10~50배 느려지므로 짧은 실행에만 적합합니다.

# 프로파일링 실행
valgrind --tool=callgrind ./myapp

# 결과 분석
callgrind_annotate callgrind.out.12345

# GUI 도구
kcachegrind callgrind.out.12345

Callgrind 옵션:

# 캐시 시뮬레이션 포함
valgrind --tool=callgrind --cache-sim=yes ./myapp

# 특정 함수만 프로파일
valgrind --tool=callgrind --toggle-collect=processData ./myapp

Visual Studio Profiler

1. 메뉴: Debug → Performance Profiler
2. CPU Usage 선택
3. Start 클릭
4. 프로그램 실행
5. Hot Path와 함수별 시간 확인

도구 비교표

도구플랫폼방식오버헤드프로덕션
perfLinux샘플링낮음 (~5%)
gprofLinux/Mac인스트루멘테이션중간 (~10%)
ValgrindLinux/Mac시뮬레이션매우 높음 (10~50x)
VS ProfilerWindows샘플링낮음

Flame Graph로 시각화하기

Flame Graph는 프로파일 결과를 가로 막대 그래프로 보여줍니다. 호출 스택이 아래에서 위로 쌓이고, 너비가 CPU 사용 비율을 나타냅니다. 한눈에 병목 함수를 찾을 수 있습니다.

# perf 데이터로 Flame Graph 생성
perf record -F 99 -g ./myapp
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

Flame Graph 읽는 법:

  • 가로: 함수가 CPU 사용 시간 중 차지하는 비율
  • 세로: 호출 스택 (아래가 호출자, 위가 피호출자)
  • 넓은 막대: 가장 많은 시간을 쓰는 호출 경로
예: main → loadFile → readBuffer → memcpy
    ↑ 이 경로가 가장 넓으면 memcpy가 병목

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

# 1. FlameGraph 스크립트 설치 (한 번만)
git clone --depth 1 https://github.com/brendangregg/FlameGraph
export PATH="$PATH:$(pwd)/FlameGraph"

# 2. perf로 호출 스택 수집 (DWARF로 정확한 스택)
perf record -F 99 -g --call-graph dwarf,8192 ./myapp

# 3. SVG 화염 그래프 생성
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

# 4. 브라우저에서 열기
open flamegraph.svg   # macOS
# xdg-open flamegraph.svg  # Linux

화염 그래프에서 자주 보는 패턴:

패턴의미대응
memcpy가 넓음메모리 복사 병목버퍼 풀, zero-copy
malloc/free가 넓음할당/해제 비용객체 풀, arena
std::sort가 넓음정렬 비용정렬 제거, 부분 정렬
pthread_mutex_lock락 대기락 최소화, lock-free

4. 완전한 프로파일링 예제

프로파일링 대상 프로그램

// profile_target.cpp - perf, gprof로 분석할 대상
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
#include <iostream>

// 의도적으로 비효율적인 함수: 캐시 미스 유발
void processDataCacheUnfriendly(std::vector<int>& data) {
    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) {
    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) {
    std::sort(data.begin(), data.end());
}

// 병목 후보: 난수 생성
void fillRandom(std::vector<int>& data) {
    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);

    fillRandom(data);
    sortData(data);
    processDataCacheUnfriendly(data);
    processDataCacheFriendly(data);

    return 0;
}

perf로 완전한 프로파일링 예제

# 1. 디버그 심볼 포함하여 컴파일
g++ -std=c++17 -O2 -g -o profile_target profile_target.cpp

# 2. perf로 호출 그래프 수집 (화염 그래프용)
perf record -F 99 -g --call-graph dwarf,8192 ./profile_target

# 3. perf report로 결과 확인
perf report --stdio

# 4. perf stat로 CPU/캐시 통계
perf stat -e cycles,instructions,cache-references,cache-misses ./profile_target

perf report —stdio 출력 예시:

# Samples: 1,234 of event 'cpu-clock'
# Event count (approx.): 12340000000
#
# Overhead  Command        Shared Object     Symbol
# ........  .............  ................  .......................
#   45.23%  profile_target profile_target    [.] sortData
#   28.10%  profile_target profile_target    [.] fillRandom
#   12.30%  profile_target profile_target    [.] processDataCacheUnfriendly
#   10.00%  profile_target profile_target    [.] processDataCacheFriendly

핫스팟 해석: sortData가 45%로 가장 큰 병목 → 정렬 알고리즘 개선 또는 정렬 제거 검토.

gprof로 완전한 프로파일링 예제

# 1. -pg 옵션으로 컴파일
g++ -std=c++17 -O2 -pg -g -o profile_target_gprof profile_target.cpp

# 2. 실행 (gmon.out 자동 생성)
./profile_target_gprof

# 3. 플랫 프로파일 (함수별 시간 비율)
gprof -p profile_target_gprof gmon.out

# 4. 호출 그래프만 보기
gprof -q profile_target_gprof gmon.out

# 5. 전체 리포트를 파일로 저장
gprof profile_target_gprof 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(호출 횟수).

핫스팟 식별 워크플로우

flowchart TD
    A[프로그램 실행] --> B[perf record -g]
    B --> C[perf report]
    C --> D{상위 3개 함수 확인}
    D --> E[넓은 막대 = 병목]
    E --> F[Timer로 세부 측정]
    F --> G[최적화 대상 구간 확정]
    G --> H[최적화 후 재측정]

5. 병목 지점 분석

핫스팟 찾기

// 프로파일 결과:
// 80% - loadFile()      ← 병목!
// 15% - processData()
// 5%  - saveResult()

// loadFile() 최적화에 집중
void loadFile(const std::string& path) {
    Timer timer("loadFile");

    // 세부 측정
    {
        Timer t("open");
        file.open(path);
    }

    {
        Timer t("read");
        // 읽기...  ← 여기가 느림!
    }

    {
        Timer t("parse");
        // 파싱...
    }
}

호출 횟수 확인

class CallCounter {
    static std::map<std::string, int> counts;
    std::string name;

public:
    CallCounter(const char* n) : name(n) {
        counts[name]++;
    }

    static void report() {
        for (const auto& [name, count] : counts) {
            std::cout << name << ": " << count << " calls\n";
        }
    }
};

std::map<std::string, int> CallCounter::counts;

void expensiveFunction() {
    CallCounter counter("expensiveFunction");
    // ...
}

int main() {
    // ...
    CallCounter::report();
}

파레토 원칙 (80/20 법칙)

전체 실행 시간의 80% ← 상위 20% 함수만 최적화
나머지 20% ← 나머지 80% 함수는 건드리지 않아도 됨

실전: 프로파일 결과에서 상위 20% 함수만 골라 최적화하면, 전체 성능의 대부분을 개선할 수 있습니다.


6. 실전 최적화 프로세스

1단계: 측정

// 현재 성능 측정
void benchmark() {
    Timer timer("Total");

    for (int i = 0; i < 1000; ++i) {
        processData();
    }
}

int main() {
    benchmark();  // 기준선 확립
}

2단계: 프로파일링

# CPU 프로파일
perf record -g ./myapp

# 결과 확인
perf report

3단계: 병목 최적화

// 병목: 벡터 재할당
void processData() {
    std::vector<int> data;  // ❌ 재할당 반복
    for (int i = 0; i < 100000; ++i) {
        data.push_back(i);
    }
}

// 최적화
void processDataOptimized() {
    std::vector<int> data;
    data.reserve(100000);  // ✅ 미리 할당
    for (int i = 0; i < 100000; ++i) {
        data.push_back(i);
    }
}

4단계: 재측정

int main() {
    {
        Timer t("Before");
        processData();  // 50ms
    }

    {
        Timer t("After");
        processDataOptimized();  // 10ms (5배 빠름)
    }
}

5단계: 반복

측정 → 프로파일 → 최적화 → 재측정 → 반복

실전 예시: JSON 파서 최적화

시나리오: 대용량 JSON 파일 파싱이 10초 걸립니다. 어디가 느린지 모릅니다.

1단계: 프로파일링:

perf record -g ./json_parser large_file.json
perf report

결과: parseJson() 80%, loadFile() 15%, validate() 5%

2단계: 파싱 세부 분석:

void parseJson(const std::string& content) {
    {
        Timer t("tokenize");
        auto tokens = tokenize(content);  // 60% 여기!
    }
    {
        Timer t("build_tree");
        buildTree(tokens);  // 20%
    }
    {
        Timer t("validate");
        validate();
    }
}

3단계: tokenize 최적화 (정규식 → 수동 파싱):

// ❌ 느림: 정규식 매칭
std::regex number_regex(R"(\d+)");
for (auto token : tokens) {
    std::regex_match(token, number_regex);
}

// ✅ 빠름: 수동 파싱
std::vector<int> parseNumbers(const std::string& s) {
    std::vector<int> result;
    size_t i = 0;
    while (i < s.size()) {
        if (std::isdigit(s[i])) {
            int num = 0;
            while (i < s.size() && std::isdigit(s[i])) {
                num = num * 10 + (s[i++] - '0');
            }
            result.push_back(num);
        } else {
            ++i;
        }
    }
    return result;
}

4단계: 재측정 → 10초 → 3초로 개선

벤치마크 시 주의사항

1. 워밍업: 첫 실행은 캐시 미스·JIT 등으로 느릴 수 있습니다.

// 워밍업 후 측정
for (int i = 0; i < 3; ++i) {
    processData();  // 캐시 워밍업
}
{
    Timer t("benchmark");
    for (int i = 0; i < 1000; ++i) {
        processData();
    }
}

2. 여러 번 실행해 평균: 한 번의 결과는 노이즈에 영향받습니다.

std::vector<long long> times;
for (int run = 0; run < 10; ++run) {
    auto start = std::chrono::high_resolution_clock::now();
    processData();
    auto end = std::chrono::high_resolution_clock::now();
    times.push_back(std::chrono::duration_cast<std::chrono::microseconds>(end - start).count());
}
// 중앙값 또는 평균 사용

3. 컴파일 최적화: -O2 또는 -O3로 릴리즈 빌드 후 측정해야 실제 성능을 반영합니다.

# 프로덕션과 동일한 최적화로 벤치마크
g++ -O2 -DNDEBUG main.cpp -o myapp

메모리 프로파일링 (Valgrind Memcheck)

CPU뿐 아니라 메모리 누수잘못된 접근도 프로파일링할 수 있습니다.

# 메모리 누수 검사
valgrind --leak-check=full ./myapp

# 출력 예시:
# ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 2
# ==12345==    at 0x4C2AB80: malloc
# ==12345==    by 0x400567: loadData() (main.cpp:42)

AddressSanitizer (ASan)는 컴파일 시 삽입되는 방식으로, Valgrind보다 빠릅니다.

# -fsanitize=address로 컴파일
g++ -g -O1 -fsanitize=address -fno-omit-frame-pointer main.cpp -o myapp
./myapp  # 힙 버퍼 오버플로우 등 즉시 감지

7. 자주 발생하는 문제

문제 1: perf 권한 오류

증상:

Permission denied (raw syscall access denied)

원인: perf는 커널 레벨 이벤트를 사용하므로 root 권한 또는 perf_event_paranoid 설정이 필요합니다.

해결법:

# 임시로 paranoid 값 낮추기 (재부팅 시 초기화)
sudo sysctl -w kernel.perf_event_paranoid=-1

# 권한 설정으로 재실행
sudo perf record ./myapp

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

원인: -pg 플래그 없이 컴파일했거나, 정상 종료가 아닌 경우(예: Ctrl+C, abort()).

해결법:

# -pg 플래그 확인
g++ -pg -O2 main.cpp -o myapp

# 정상 종료되도록 main에서 return 0
exit(0);  // 또는 정상 종료 경로

문제 3: Valgrind가 너무 느림

원인: 시뮬레이션 방식이라 10~50배 느립니다.

해결법:

# 입력 데이터를 작게 줄여서 테스트
./myapp < small_input.txt

# 또는 perf로 대체 (CPU 프로파일링만 필요할 때)
perf record ./myapp

문제 4: 프로파일 결과에 함수명이 없음 (???)

원인: 디버그 심볼이 없거나 릴리즈 빌드에서 strip된 경우.

해결법:

# -g 옵션으로 디버그 심볼 포함
g++ -g -O2 main.cpp -o myapp

# strip하지 않기
# g++ -g -O2 main.cpp -o myapp  # -s 옵션 제거

문제 5: 컴파일 최적화로 인라인된 함수

증상: 프로파일에서 main만 보이고 세부 함수가 보이지 않음.

원인: -O2/-O3에서 인라인 최적화로 인해 함수가 합쳐짐.

해결법:

# 프로파일링용 빌드는 -O1 또는 -O0
g++ -g -O1 -pg main.cpp -o myapp

# 또는 __attribute__((noinline))로 특정 함수 보호
void __attribute__((noinline)) criticalFunction() { ... }

7. 체크리스트

프로파일링 전 체크리스트

  • -g 플래그로 디버그 심볼 포함
  • 최적화 수준 결정 (-O1 권장, 정확한 호출 관계 필요 시 -O0)
  • perf 사용 시 perf_event_paranoid 설정 확인
  • gprof 사용 시 -pg 플래그 추가
  • Valgrind 사용 시 입력 데이터 크기 축소

프로파일링 후 체크리스트

  • 상위 20% 함수 식별
  • 병목 구간 세부 측정 (Timer 등)
  • 최적화 전 기준선 기록
  • 최적화 후 재측정
  • 회귀 테스트 (기능 정상 동작 확인)

최적화 원칙 체크리스트

  • 추측하지 말고 측정
  • 병목 80%부터 최적화
  • 최적화 전후 비교
  • 작은 개선보다 큰 병목
  • 프로파일러 활용

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기
  • C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
  • C++ 컴파일러 최적화 | PGO·LTO로 “느린 프로그램” 성능 30% 향상시키기

이 글에서 다루는 키워드 (관련 검색어)

C++ 프로파일링, perf gprof, Valgrind, 병목 지점, 성능 측정, 최적화 등으로 검색하시면 이 글이 도움이 됩니다.

정리

도구플랫폼용도
perfLinuxCPU 프로파일링
gprofLinux/Mac함수별 시간
ValgrindLinux/Mac메모리, 캐시
VS ProfilerWindowsCPU, 메모리
std::chrono모든 플랫폼수동 측정

핵심 원칙:

  1. 추측 금지, 측정 필수
  2. 병목 80%부터 최적화
  3. 최적화 전후 비교
  4. 작은 개선보다 큰 병목
  5. 프로파일러 활용

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 프로파일링 도구(perf, gprof, Valgrind)로 병목 지점을 찾고, 성능을 측정하며, 실전에서 최적화 포인트를 발견하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. perf와 gprof 중 어떤 것을 써야 하나요?

A. Linux에서는 perf를 우선 추천합니다. 샘플링 방식이라 오버헤드가 적고, 프로덕션 환경에서도 사용 가능합니다. gprof는 -pg 플래그로 코드를 수정해야 하므로, 빌드 설정이 다를 수 있습니다. 정확한 호출 그래프가 필요하면 Valgrind Callgrind를 고려하세요.

Q. 프로파일링이 프로그램을 느리게 만들지 않나요?

A. perf는 샘플링 방식이라 오버헤드가 5% 내외로 낮습니다. gprof는 인스트루멘테이션이라 1020% 정도 느려질 수 있지만, 병목 찾기에는 충분합니다. Valgrind는 시뮬레이션이라 1050배 느려지므로, 짧은 실행에만 사용하세요.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

한 줄 요약: chrono나 프로파일러로 병목 구간을 찾은 뒤 최적화할 수 있습니다. 다음으로 캐시 친화적 코드(#15-2)를 읽어보면 좋습니다.

다음 글: [C++ 실전 가이드 #15-2] 캐시 친화적 코드: 메모리 접근 패턴 최적화

이전 글: [C++ 실전 가이드 #14-2] Perfect Forwarding과 std::forward


관련 글

  • C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기
  • C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
  • C++ 프로그램 느림 원인 찾기 | 프로파일링으로 병목 5분 만에 찾는 법
  • C++ 고급 프로파일링 완벽 가이드 | perf·gprof
  • C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드