icon
16장 : 성능 최적화와 디버깅 기법

성능 프로파일링 도구 소개

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

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

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

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


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

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

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

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

성능 프로파일링의 종류

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

  1. CPU 프로파일링 (CPU Profiling)

    • 프로그램이 CPU 시간을 어디에 가장 많이 사용하는지 분석합니다.
    • 함수 호출 시간, 호출 빈도 등을 측정하여 CPU 병목을 식별합니다.
    • gprof (Linux), perf (Linux), Visual Studio Profiler (Windows), Apple Instruments (macOS) 등.
  2. 메모리 프로파일링 (Memory Profiling)

    • 프로그램이 메모리를 어떻게 할당하고 사용하는지 분석합니다.
    • 메모리 누수(memory leak), 과도한 메모리 할당/해제, 비효율적인 메모리 사용 패턴 등을 발견합니다.
    • Valgrind (Linux), Visual Studio Diagnostic Tools (Windows), Purify 등.
  3. I/O 프로파일링 (I/O Profiling)

    • 디스크 I/O나 네트워크 I/O와 관련된 병목을 분석합니다.
    • 읽기/쓰기 작업의 양, 지연 시간 등을 측정합니다.

프로파일링 도구 (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 프로파일링은 일반적으로 다음과 같은 단계를 따릅니다.

  1. 프로파일링 빌드

    • 최적화(-O2, -O3 등)를 켜고 디버깅 심볼(-g for GCC/Clang, /Zi for MSVC)을 포함하여 빌드합니다. 최적화 없이 프로파일링하면 실제 릴리스 빌드의 성능과 다를 수 있습니다.
    • 최적화를 켜야 하는 이유: 컴파일러의 최적화가 실제 성능에 미치는 영향이 크기 때문입니다.
    • 디버깅 심볼을 포함해야 하는 이유: 프로파일러가 실행 주소를 소스 코드의 함수 이름과 라인 번호에 매핑할 수 있도록 하기 위함입니다.
  2. 프로파일러 실행

    • 프로파일러를 사용하여 프로그램을 실행합니다.
    • 예를 들어, gprof의 경우
      g++ -O2 -g 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 내에서 프로파일링 기능을 시작합니다.
  3. 데이터 수집

    • 프로파일러는 프로그램이 실행되는 동안 CPU 시간 샘플링, 함수 호출 그래프 추적 등의 방법으로 데이터를 수집합니다.
    • 핵심: 충분한 시간 동안 프로파일링을 실행하여 유의미한 데이터를 얻어야 합니다. 너무 짧게 실행하면 일시적인 부하나 초기화 과정에 의해 결과가 왜곡될 수 있습니다.
  4. 결과 분석

    • 수집된 데이터를 프로파일러가 제공하는 리포트나 시각화 도구를 통해 분석합니다.
    • 가장 많은 시간을 소비하는 함수(hotspot)를 식별합니다.
    • 함수 호출 그래프(call graph)를 통해 어떤 함수가 어떤 함수를 호출하여 병목이 발생하는지 파악합니다.
    • 메모리 프로파일러의 경우, 가장 많은 메모리를 할당하는 부분이나 메모리 누수 지점을 찾습니다.
  5. 병목 개선 및 재측정

    • 식별된 병목 지점을 개선하기 위한 코드를 수정합니다.
      • 더 효율적인 알고리즘 사용
      • 데이터 구조 변경
      • 불필요한 메모리 할당/해제 줄이기
      • 캐시 친화적인 코드 작성
      • 병렬 처리 도입 등
    • 수정 후 다시 프로파일링을 수행하여 개선 효과를 측정합니다. 성능이 충분히 개선될 때까지 이 과정을 반복합니다.

자가 측정 (Manual Profiling)

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

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

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

void 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 집약적 작업
    // ... 더 복잡한 연산
}

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

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

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

    // 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::high_resolution_clock은 시스템에서 사용 가능한 가장 정밀한 시계입니다. 이 방법은 특정 함수의 성능을 대략적으로 측정하고, 최적화 전후의 변화를 비교하는 데 유용합니다. 하지만 이는 전체 프로그램의 병목을 찾는 데는 한계가 있으며, 전문 프로파일러를 대체할 수는 없습니다.


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

특히,

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

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

이 장에서 배운 프로파일링의 기본 개념을 바탕으로, 여러분이 작성한 C++ 코드가 최고의 성능을 발휘할 수 있도록 노력하시기를 바랍니다.

다음 장에서는 프로그램의 논리적 오류를 찾아 수정하는 데 필수적인 디버깅 기법에 대해 학습하겠습니다.