본문 바로가기

성능 프로파일링 도구 소개

CPU 시간·메모리·호출 빈도를 측정하는 프로파일러와 수동 계측을 활용해 추측이 아닌 데이터로 병목을 찾습니다.

프로그램을 단순히 동작하게 만드는 것과 최적의 성능으로 동작하게 만드는 것 사이에는 큰 차이가 있습니다.

특히 C++은 성능이 중요한 시스템 프로그래밍, 게임 개발, 고성능 컴퓨팅(HPC)에서 많이 사용되므로, 코드 성능을 측정하고 개선하는 능력이 중요합니다.

이번 장에서는 성능 병목(bottleneck)을 식별하고 개선하기 위한 첫걸음인 성능 프로파일링(Performance Profiling)을 다룹니다.

프로파일링은 코드의 어느 부분이 가장 많은 시간을 소비하는지, 어떤 함수가 자주 호출되는지 등을 분석하여 최적화의 대상을 명확히 하는 과정입니다.

먼저 측정하려는 시간을 정합니다. 요청을 시작해 끝낼 때까지의 경과 시간에는 CPU 실행뿐 아니라 I/O·잠금 대기도 들어갑니다. CPU 시간은 실행에 사용한 시간이며, 샘플링 프로파일러의 함수 비율은 그 도구가 모은 이벤트 표본의 비율입니다. CPU 샘플이 적은 함수도 오래 기다릴 수 있으므로, 함수 비율 하나를 전체 요청 지연의 비율로 바꾸어 읽지 않습니다.

먼저 이 절에서 다룰 기준, 프로파일링 종류, 대표 도구를 한 화면에서 묶어 보면 다음과 같습니다.

프로파일링 기록은 한 변경과 재측정마다 갱신된다

입력과 환경 고정→기준 측정→병목 가설→한 가지 변경→동일 조건 재측정의 작업 상태가 실제로 바뀐다.

C++ · MEASURE AND VERIFY

측정 조건과 결과를 기록하며 한 변경씩 재검증한다

같은 입력·빌드·환경에서 비용을 측정하고 가설 하나를 바꾼다. 단계마다 조건과 결과를 기록하여 다음 측정과 비교한다.

측정 조건과 결과를 기록하며 한 변경씩 재검증한다다섯 단계의 실선 화살표를 시계 방향으로 따라간 뒤 첫 단계로 돌아간다. 각 단계에서 중앙의 공통 기록으로 향하는 점선 화살표는 조건과 결과를 남김을 뜻한다. 기록은 여섯 번째 실행 단계가 아니다.1. 기준선 고정입력 · 빌드환경 고정2. 지표 측정CPU · 메모리대기 비용3. hotspot 선택가장 큰 비용원인 가설4. 한 변경선택한 가설하나만 적용5. 재검증정확성 확인동일 조건 측정성능 기록조건 · 분포정확성
측정 조건과 결과를 기록하며 한 변경씩 재검증한다위에서 아래로 같은 다섯 단계를 진행하고 왼쪽 실선으로 첫 단계에 돌아간다. 각 단계의 오른쪽 점선은 하나의 공통 기록 영역을 갱신한다. 기록을 마지막 단계로 오해하지 않도록 별도 영역으로 둔다.다음1. 기준선입력 · 빌드환경 고정2. 지표 측정CPU · 메모리대기 비용3. 병목 선택큰 비용원인 가설4. 한 변경가설 하나적용5. 재검증정확성동일 측정공통기록조건분포정답

기준선 고정

입력 · 빌드 · 환경

지표 측정

CPU · 메모리 · 대기

hotspot 선택

가장 큰 비용 · 원인 가설

한 변경

선택한 가설 하나만

재검증

정확성 + 동일 조건 측정

최적화와 디버그 심볼을 포함한 빌드로 대표 작업을 충분히 측정한다. 한 번의 숫자 대신 반복 분포와 정답을 비교하며 기록을 갱신한다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석같은 입력·빌드·환경에서 비용을 측정하고 가설 하나에 따른 변경을 적용한다. 단계마다 조건과 결과를 기록하여 다음 측정과 비교한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
기준 측정

같은 입력·환경으로 기준 실행을 기록한다.

  • 조건: 입력·환경 고정
  • 산출물: 기준 측정
  • 후속 결과를 비교하려면 조건과 기준 근거가 먼저 필요하다.
한 가설·한 변경

프로파일의 병목 근거에 따라 하나만 바꾼다.

  • 입력 근거: 병목 측정
  • 변경 수: 한 가지
  • 여러 변경을 섞으면 어떤 변화가 결과를 만든 것인지 알기 어렵다.
재측정

같은 조건에서 정확성과 성능을 다시 확인한다.

  • 비교: 기준 ↔ 변경 후
  • 결정: 근거에 따라 유지·되돌림
  • 실측 결과가 가설을 지지하는지 판단하며 속도 향상을 미리 단정하지 않는다.

프로파일링 기록은 한 변경과 재측정마다 갱신된다

입력과 환경 고정→기준 측정→병목 가설→한 가지 변경→동일 조건 재측정의 작업 상태가 실제로 바뀐다.

C++ · MEASURE AND VERIFY

측정 조건과 결과를 기록하며 한 변경씩 재검증한다

같은 입력·빌드·환경에서 비용을 측정하고 가설 하나를 바꾼다. 단계마다 조건과 결과를 기록하여 다음 측정과 비교한다.

측정 조건과 결과를 기록하며 한 변경씩 재검증한다다섯 단계의 실선 화살표를 시계 방향으로 따라간 뒤 첫 단계로 돌아간다. 각 단계에서 중앙의 공통 기록으로 향하는 점선 화살표는 조건과 결과를 남김을 뜻한다. 기록은 여섯 번째 실행 단계가 아니다.1. 기준선 고정입력 · 빌드환경 고정2. 지표 측정CPU · 메모리대기 비용3. hotspot 선택가장 큰 비용원인 가설4. 한 변경선택한 가설하나만 적용5. 재검증정확성 확인동일 조건 측정성능 기록조건 · 분포정확성
측정 조건과 결과를 기록하며 한 변경씩 재검증한다위에서 아래로 같은 다섯 단계를 진행하고 왼쪽 실선으로 첫 단계에 돌아간다. 각 단계의 오른쪽 점선은 하나의 공통 기록 영역을 갱신한다. 기록을 마지막 단계로 오해하지 않도록 별도 영역으로 둔다.다음1. 기준선입력 · 빌드환경 고정2. 지표 측정CPU · 메모리대기 비용3. 병목 선택큰 비용원인 가설4. 한 변경가설 하나적용5. 재검증정확성동일 측정공통기록조건분포정답

기준선 고정

입력 · 빌드 · 환경

지표 측정

CPU · 메모리 · 대기

hotspot 선택

가장 큰 비용 · 원인 가설

한 변경

선택한 가설 하나만

재검증

정확성 + 동일 조건 측정

최적화와 디버그 심볼을 포함한 빌드로 대표 작업을 충분히 측정한다. 한 번의 숫자 대신 반복 분포와 정답을 비교하며 기록을 갱신한다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석같은 입력·빌드·환경에서 비용을 측정하고 가설 하나에 따른 변경을 적용한다. 단계마다 조건과 결과를 기록하여 다음 측정과 비교한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
기준 측정

같은 입력·환경으로 기준 실행을 기록한다.

  • 조건: 입력·환경 고정
  • 산출물: 기준 측정
  • 후속 결과를 비교하려면 조건과 기준 근거가 먼저 필요하다.
한 가설·한 변경

프로파일의 병목 근거에 따라 하나만 바꾼다.

  • 입력 근거: 병목 측정
  • 변경 수: 한 가지
  • 여러 변경을 섞으면 어떤 변화가 결과를 만든 것인지 알기 어렵다.
재측정

같은 조건에서 정확성과 성능을 다시 확인한다.

  • 비교: 기준 ↔ 변경 후
  • 결정: 근거에 따라 유지·되돌림
  • 실측 결과가 가설을 지지하는지 판단하며 속도 향상을 미리 단정하지 않는다.

왜 성능 프로파일링이 필요한가?

성능 최적화는 측정하지 않고 하지 마라(Don't optimize without measuring)라는 유명한 말이 있습니다.

개발자들은 흔히 자신의 직관이나 경험에 의존하여 성능 문제를 예측하려 하지만, 실제 병목 지점은 예상치 못한 곳에 있을 때가 많습니다.

  • 잘못된 최적화 방지: 직관에 의존한 최적화는 실제 성능에 영향을 미치지 않거나, 심지어 성능을 저하시키고 코드를 복잡하게 만들 수 있습니다.
  • 리소스 낭비 방지: 모든 코드를 최적화하는 것은 시간과 노력이 많이 듭니다. 프로파일링을 통해 가장 큰 영향을 미치는 부분에만 노력을 집중할 수 있습니다.
  • 객관적인 데이터 제공: 프로파일링 데이터는 이 부분이 느리다는 주관적인 판단 대신, 객관적인 측정 지표를 제공합니다.
  • 숨겨진 문제 발견: 의도치 않은 메모리 할당/해제, 과도한 함수 호출, 캐시 미스(cache miss) 등 숨겨진 성능 저하 요인을 발견할 수 있습니다.

성능 프로파일링의 종류

다양한 프로파일링 기법이 있으며, 목적에 따라 적절한 기법을 선택해야 합니다.

CPU 프로파일링 (CPU Profiling)
  • 프로그램이 CPU 시간을 어디에 가장 많이 사용하는지 분석합니다.
  • 함수 호출 시간, 호출 빈도 등을 측정하여 CPU 병목을 식별합니다.
  • gprof (Linux), perf (Linux), Visual Studio Profiler (Windows), Apple Instruments (macOS) 등.
메모리 프로파일링 (Memory Profiling)
  • 프로그램이 메모리를 어떻게 할당하고 사용하는지 분석합니다.
  • 메모리 누수(memory leak), 과도한 메모리 할당/해제, 비효율적인 메모리 사용 패턴 등을 발견합니다.
  • Valgrind (Linux), Visual Studio Diagnostic Tools (Windows), Purify 등.
I/O 프로파일링 (I/O Profiling)
  • 디스크 I/O나 네트워크 I/O와 관련된 병목을 분석합니다.
  • 읽기/쓰기 작업의 양, 지연 시간 등을 측정합니다.

아래 다이어그램은 성능 병목의 증상에 따라 어떤 프로파일링 관점과 지표를 먼저 볼지 정리한 것입니다.

C++ · PROFILING QUESTIONS

증상에 맞는 지표로 성능 가설을 검증한다

도구 이름을 고르기 전에 무엇이 느린지 정한다. 다음 네 관점은 함께 나타날 수 있으므로 측정 결과로 가설을 좁힌다.

CPU가 바쁨 → 실행 비용

샘플과 호출 그래프로 시간을 많이 쓰는 함수와 호출 경로를 찾는다.

호출 횟수와 한 번의 비용을 구분한다. CPU 사용량만으로 원인 함수를 단정하지 않는다.

메모리가 증가 → 할당과 수명

할당 위치·빈도·최대 사용량과 살아 있는 객체의 수명을 확인한다.

사용량 증가 자체가 누수의 증명은 아니다. 정상 캐시·버퍼 보유와 잃어버린 해제 책임을 구별한다.

CPU는 남는데 느림 → 대기

I/O·큐·잠금 대기 시간과 실행 타임라인을 확인한다.

CPU hotspot만 보면 대기 병목을 놓칠 수 있다. 요청의 전체 경과 시간과 실제 실행 시간을 비교한다.

작은 구간 차이 → 반복 측정

같은 작업의 실행 시간을 반복 수집해 분포와 편차를 비교한다.

const auto start =
  std::chrono::steady_clock::now();
// 관찰할 작업
const auto end =
  std::chrono::steady_clock::now();

본문 chrono 예제는 결과 checksum을 사용한다. 초기화·출력 비용과 측정할 범위를 함께 확인한다.

profiler는 비용 측정, sanitizer는 오류 탐지를 주로 맡는다. 도구의 계측 비용·플랫폼·빌드 조건을 기록하고 같은 조건에서 해석한다.


프로파일링 도구 (Profilers)

운영체제와 개발 환경에 따라 다양한 프로파일링 도구들이 있습니다.

  • Linux
    • gprof: GNU 컴파일러 모음(GCC)과 함께 제공되는 기본적인 CPU 프로파일러. 함수 호출 시간 및 호출 그래프를 분석합니다.
    • perf: Linux 커널에서 제공하는 성능 분석 도구. CPU 이벤트, 캐시 미스, 스케줄링 지연 등 다양한 하드웨어 및 소프트웨어 이벤트를 추적합니다.
    • Valgrind (Callgrind): 메모리 오류 검사 도구로 유명하지만, Callgrind라는 하위 도구를 통해 CPU 프로파일링도 가능합니다. (실행 속도가 느려짐)
    • oprofile: 시스템 전반의 프로파일링이 가능합니다.
  • Windows (Visual Studio)
    • Visual Studio IDE에 통합된 성능 프로파일러(Diagnostic Tools): CPU 사용량, 메모리 사용량, 이벤트, 파일 I/O 등 다양한 항목을 시각적으로 분석합니다.
    • Intel VTune Amplifier: Intel CPU에 최적화된 고급 프로파일러.
  • macOS (Xcode)
    • Instruments: Xcode에 포함된 프로파일링 도구. CPU, 메모리, 그래픽, 네트워크 등 다양한 측면을 분석합니다.
  • 크로스 플랫폼/상용 도구
    • Google gperftools (CPU profiler): Google에서 개발한 고성능 CPU 프로파일러.
    • Remotery: 게임 개발에 주로 사용되는 실시간 인게임 프로파일러.
    • PAPI (Performance Application Programming Interface): 하드웨어 성능 카운터에 접근하여 CPU 병목을 더 세밀하게 분석.

이 책에서는 특정 도구의 상세 사용법보다는 프로파일링의 개념과 접근 방식을 이해하는 데 중점을 둡니다.

각 도구의 사용법은 해당 도구의 공식 문서를 참조하는 것이 가장 좋습니다.


기본적인 CPU 프로파일링 과정

CPU 프로파일링은 일반적으로 다음과 같은 단계를 따릅니다.

프로파일링 빌드
  • 최적화(-O2, -O3 등)를 켜고 디버깅 심볼(GCC/Clang의 -g, MSVC의 /Zi)을 포함하여 빌드합니다. 최적화 없이 프로파일링하면 실제 릴리스 빌드의 성능과 다를 수 있습니다.
  • 최적화를 켜야 하는 이유: 컴파일러의 최적화가 실제 성능에 미치는 영향이 크기 때문입니다.
  • 디버깅 심볼을 포함해야 하는 이유: 프로파일러가 실행 주소를 소스 코드의 함수 이름과 라인 번호에 매핑할 수 있도록 하기 위함입니다.
프로파일러 실행
  • 프로파일러를 사용하여 프로그램을 실행합니다.
  • 예를 들어, gprof의 경우
    g++ -O2 -g -pg my_program.cpp -o my_program
    ./my_program  # gmon.out 파일 생성
    gprof my_program gmon.out > analysis.txt
  • perf의 경우
    perf record -g ./my_program # perf.data 파일 생성
    perf report -g
  • Visual Studio나 Instruments는 IDE 내에서 프로파일링 기능을 시작합니다.
데이터 수집
  • 프로파일러는 프로그램이 실행되는 동안 CPU 시간 샘플링, 함수 호출 그래프 추적 등의 방법으로 데이터를 수집합니다.
  • 핵심: 충분한 시간 동안 프로파일링을 실행하여 유의미한 데이터를 얻어야 합니다. 너무 짧게 실행하면 일시적인 부하나 초기화 과정에 의해 결과가 왜곡될 수 있습니다.
결과 분석
  • 수집된 데이터를 프로파일러가 제공하는 리포트나 시각화 도구를 통해 분석합니다.
  • 가장 많은 시간을 소비하는 함수(hotspot)를 식별합니다.
  • 함수 호출 그래프(call graph)를 통해 어떤 함수가 어떤 함수를 호출하여 병목이 발생하는지 파악합니다.
  • 메모리 프로파일러의 경우, 가장 많은 메모리를 할당하는 부분이나 메모리 누수 지점을 찾습니다.
병목 개선 및 재측정
  • 식별된 병목 지점을 개선하기 위한 코드를 수정합니다.
    • 더 효율적인 알고리즘 사용
    • 데이터 구조 변경
    • 불필요한 메모리 할당/해제 줄이기
    • 캐시 친화적인 코드 작성
    • 병렬 처리 도입 등
  • 수정 후 다시 프로파일링을 수행하여 개선 효과를 측정합니다. 성능이 충분히 개선될 때까지 이 과정을 반복합니다.

자가 측정 (Manual Profiling)

전문 프로파일러를 사용하기 전에, 코드의 특정 부분의 성능을 대략적으로 측정해야 할 때가 있습니다.

C++의 <chrono> 라이브러리를 사용하면 코드 블록의 실행 시간을 쉽게 측정할 수 있습니다.

std::chrono를 이용한 코드 실행 시간 측정
#include <iostream>
#include <chrono> // 시간을 측정하기 위한 헤더
#include <vector>
#include <algorithm> // std::sort

int perform_heavy_calculation() {
    std::vector<int> data(1000000);
    // 데이터를 채우고 복잡한 연산 수행
    for (int i = 0; i < 1000000; ++i) {
        data[i] = (i * 13) % 1000;
    }
    std::sort(data.begin(), data.end()); // 정렬은 CPU 집약적 작업
    return data.front() + data[data.size() / 2] + data.back();
}

int main() {
    // 1. 시작 시간 기록
    auto start = std::chrono::steady_clock::now();

    // 2. 측정하고자 하는 코드 블록
    const int checksum = perform_heavy_calculation();

    // 3. 종료 시간 기록
    auto end = std::chrono::steady_clock::now();

    std::cout << "Result checksum: " << checksum << std::endl;

    // 4. 경과 시간 계산
    // std::chrono::duration_cast를 사용하여 원하는 단위로 변환
    std::chrono::duration<double> duration = end - start; // 초 단위 (double)
    std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::cout << "Function took: " << duration.count() << " seconds." << std::endl;
    std::cout << "Function took: " << ms.count() << " milliseconds." << std::endl;

    return 0;
}

std::chrono::steady_clock은 시간이 역행하지 않는 시계로 경과 시간 측정에 적합합니다. 해상도와 실제 정밀도는 구현에 따라 다릅니다.

예시

앞의 완전한 프로그램을 별도 파일로 저장해 C++11 이상으로 빌드합니다. 정렬할 벡터의 할당·초기화·정렬은 시작과 종료 사이에 있어 측정에 포함되고, 세 줄의 출력은 종료 시각을 기록한 뒤라 포함되지 않습니다. 0부터 999까지의 값은 각각 1,000개씩 만들어지므로 정렬 후 처음·중간·마지막 값은 0·500·999, checksum은 1499입니다. 더 빠른 버전을 비교할 때도 이 결과를 먼저 확인합니다. 출력이 사용된다는 사실만으로 어떤 컴파일러 최적화도 일어나지 않는다고 보장할 수는 없습니다.

밀리초 정수로 변환하면 소수 부분이 잘립니다. 예를 들어 0.7 ms는 정수 출력에서 0 ms가 되지만 작업 비용이 없다는 뜻은 아닙니다. 같은 실행 파일을 여러 번 실행해 입력·빌드 옵션·환경과 각 측정값을 기록하고, 초기 실행과 반복 실행을 섞었는지도 확인합니다. 계측된 프로그램의 수치와 계측하지 않은 릴리스 프로그램의 수치를 구별합니다.

예시

실제 벤치마크 결과가 아닌 판단 연습용 자료입니다. 같은 조건의 다섯 실행에서 A는 100·101·99·100·150 ms, B는 98·102·100·99·101 ms였다고 합시다. 정렬한 가운데 값인 중앙값은 둘 다 100 ms입니다. B의 첫 실행 98 ms만 골라 2% 개선이라고 결론 내리면 나머지 반복과 변동을 무시합니다. 이 자료에서는 분포와 이상값 원인을 더 확인해야 합니다.

독립 확인: 후보 C의 다섯 값이 80·81·79·80·80 ms이고 checksum이 1498이라면, 중앙값 기준 변화율과 채택 여부를 판단하세요. checksum이 1499이고 독립 실행들에서도 같은 분포가 관찰된다면 무엇을 추가로 기록해야 하나요?

판단 과정 확인

C의 중앙값은 80 ms로, A의 100 ms보다 20% 작습니다. 하지만 checksum이 1498이면 원 프로그램의 결과 1499와 달라 정확성 기준에 실패하므로 채택하지 않습니다. 결과가 1499인 경우에도 입력 범위·빌드 옵션·측정 범위·환경·반복값과 편차를 남기고 다른 유효 입력의 정확성을 확인합니다. 다섯 값만으로 모든 환경에서의 개선을 보장하지 않습니다.

이 방법은 특정 함수의 성능을 대략적으로 측정하고, 최적화 전후의 변화를 비교하는 데 유용합니다.

하지만 이는 전체 프로그램의 병목을 찾는 데는 한계가 있으며, 전문 프로파일러를 대체할 수는 없습니다.

측정 결과를 해석할 때는 도구 이름보다 먼저 병목의 종류와 확인할 지표를 정리해야 합니다.

앞의 판단 흐름처럼 대표 증상에서 도구를 고르고 동일한 조건으로 재측정합니다.


이번 장에서는 프로그램의 성능을 분석하고 개선하기 위한 필수적인 단계인 성능 프로파일링(Performance Profiling)에 대해 학습했습니다.

특히,

  • 왜 프로파일링이 필요한지, 그리고 직관적인 최적화가 왜 위험한지.
  • CPU 프로파일링, 메모리 프로파일링 등 주요 프로파일링 종류.
  • gprof, perf, Visual Studio Profiler, Instruments와 같은 주요 프로파일링 도구.
  • 일반적인 프로파일링 과정 (빌드, 실행, 수집, 분석, 개선).
  • std::chrono를 이용한 자가 측정 방법.

성능 최적화는 지속적인 측정과 반복적인 개선의 과정입니다.

프로파일링의 기본 개념을 바탕으로, 작성한 C++ 코드의 병목을 측정하고 개선할 수 있습니다.

다음 절에서는 확인한 병목에 적용할 코드 최적화 기법을 다룹니다.

프로파일링 결과를 믿으려면 도구 이름보다 측정 조건을 고정하고, 하나씩 바꾸고, 같은 조건에서 다시 확인하는 습관이 더 중요합니다.