icon안동민 개발노트

성능 프로파일링 도구 소개


프로파일링의 기본 개념

 성능 프로파일링은 프로그램의 성능을 분석하고 최적화하는 과정에서 핵심적인 역할을 합니다.

 이 장에서는 C++ 프로그램의 성능을 향상시키기 위한 다양한 프로파일링 도구와 기법을 자세히 살펴보겠습니다.

 프로파일링은 프로그램의 실행 시간, 메모리 사용량, CPU 사용률, 함수 호출 빈도 등을 측정하고 분석하는 과정입니다.

 주요 목적은 다음과 같습니다.

  1. 성능 병목 지점 식별
  2. 리소스 사용량 분석
  3. 알고리즘 및 데이터 구조의 효율성 평가
  4. 최적화 대상 우선순위 결정

 프로파일링의 종류

  1. 정적 프로파일링 : 코드 분석을 통해 잠재적인 성능 문제를 파악합니다.
  2. 동적 프로파일링 : 프로그램 실행 중 실제 동작을 분석합니다.
  • 샘플링 기반 : 주기적으로 프로그램 상태를 확인합니다.
  • 계측 기반 : 코드에 프로파일링 지점을 삽입하여 상세한 정보를 수집합니다.

주요 프로파일링 도구

 gprof

 GNU Profiler(gprof)는 UNIX 계열 시스템에서 널리 사용되는 프로파일링 도구입니다.

 사용 방법

  1. 프로파일링을 위한 컴파일
g++ -pg -o myprogram myprogram.cpp
  1. 프로그램 실행
./myprogram
  1. 프로파일 데이터 분석
gprof myprogram gmon.out > analysis.txt

 장단점

  • 장점 : 사용이 간단하며, 함수 호출 그래프를 제공합니다.
  • 단점 : 컴파일 시 최적화 옵션을 사용하면 부정확할 수 있습니다.
예제
#include <iostream>
 
void function_a() {
    for(int i = 0; i < 1000000; i++) {
        // Some operation
    }
}
 
void function_b() {
    for(int i = 0; i < 500000; i++) {
        // Some operation
    }
}
 
int main() {
    function_a();
    function_b();
    return 0;
}

 이 코드를 gprof로 프로파일링하면 function_a가 더 많은 시간을 소비한다는 것을 알 수 있습니다.

 Valgrind

 Valgrind는 메모리 디버깅, 메모리 누수 감지, 캐시 프로파일링 등 다양한 도구를 제공합니다.

 주요 도구

  • Memcheck : 메모리 오류와 누수 감지
  • Cachegrind : 캐시 및 분기 예측 프로파일링
  • Callgrind : 함수 호출 그래프 및 상세한 실행 시간 분석

 사용 예 (Memcheck)

valgrind --tool=memcheck ./myprogram

 장단점

  • 장점 : 매우 상세한 메모리 분석이 가능합니다.
  • 단점 : 실행 속도가 크게 저하됩니다.
예제 : 메모리 누수 감지
// 예제 : 메모리 누수 감지
#include <cstdlib>
 
int main() {
    int* ptr = new int[10];
    // ptr을 delete하지 않고 프로그램 종료
    return 0;
}

 Valgrind Memcheck를 사용하면 이 프로그램의 메모리 누수를 감지할 수 있습니다.

 perf

 perf는 Linux 커널의 성능 분석 도구로, 하드웨어 성능 카운터를 활용합니다.

 사용 예

perf record ./myprogram
perf report

 장점

  • 시스템 수준의 프로파일링이 가능합니다.
  • 낮은 오버헤드로 실제 환경에 가까운 프로파일링이 가능합니다.

 단점

  • Linux 환경에서만 사용 가능합니다.
  • 결과 해석에 전문 지식이 필요할 수 있습니다.
예제 : CPU 사이클 측정
// 예제 : CPU 사이클 측정
#include <vector>
#include <algorithm>
 
int main() {
    std::vector<int> v(1000000);
    for(int i = 0; i < 1000000; i++) {
        v[i] = rand();
    }
    std::sort(v.begin(), v.end());
    return 0;
}

 perf를 사용하면 이 프로그램에서 std::sort 함수가 소비하는 CPU 사이클을 정확히 측정할 수 있습니다.

 Intel VTune Profiler

 Intel VTune Profiler는 매우 강력하고 상세한 성능 분석 도구입니다.

 주요 기능

  • 핫스팟 분석
  • 멀티쓰레딩 효율성 분석
  • 마이크로아키텍처 분석
  • I/O 분석

 장단점

  • 장점 : 매우 상세하고 정확한 분석이 가능합니다.
  • 단점 : 상용 도구이며, Intel 프로세서에 최적화되어 있습니다.

프로파일링 기법 및 팁

  1. 대표적인 입력 데이터 사용 : 실제 사용 환경을 반영하는 데이터로 프로파일링하세요.
  2. 릴리스 모드로 컴파일 : 최적화된 코드의 실제 성능을 측정하세요.
  3. 반복 측정 : 여러 번 측정하여 평균값을 사용하세요.
  4. 시스템 부하 최소화 : 프로파일링 중 다른 프로그램의 영향을 최소화하세요.
  5. 점진적 최적화 : 한 번에 한 가지씩 변경하고 그 효과를 측정하세요.
  6. 80-20 규칙 적용 : 대부분의 경우 20%의 코드가 80%의 실행 시간을 차지합니다. 이 부분에 집중하세요.

실습 : 간단한 프로파일링 예제

 다음 코드를 사용하여 다양한 정렬 알고리즘의 성능을 비교해봅시다.

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <random>
 
void bubbleSort(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++)
        for (int j = 0; j < n-i-1; j++)
            if (arr[j] > arr[j+1])
                std::swap(arr[j], arr[j+1]);
}
 
void insertionSort(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
}
 
int partition(std::vector<int>& arr, int low, int high) {
    int pivot = arr[high];
    int i = (low - 1);
    for (int j = low; j <= high - 1; j++) {
        if (arr[j] < pivot) {
            i++;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[high]);
    return (i + 1);
}
 
void quickSort(std::vector<int>& arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
 
std::vector<int> generateRandomVector(int size) {
    std::vector<int> vec(size);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 1000000);
 
    for (int& num : vec) {
        num = dis(gen);
    }
    return vec;
}
 
template<typename Func>
double measureTime(Func f, std::vector<int>& arr) {
    auto start = std::chrono::high_resolution_clock::now();
    f(arr);
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end - start;
    return diff.count();
}
 
int main() {
    const int SIZE = 10000;
    std::vector<int> arr = generateRandomVector(SIZE);
    std::vector<int> arr_copy = arr;
 
    std::cout << "Bubble Sort Time: " << measureTime(bubbleSort, arr) << " s\n";
    
    arr = arr_copy;
    std::cout << "Insertion Sort Time: " << measureTime(insertionSort, arr) << " s\n";
    
    arr = arr_copy;
    std::cout << "Quick Sort Time: " << measureTime([&arr](std::vector<int>& v){quickSort(v, 0, v.size()-1);}, arr) << " s\n";
    
    arr = arr_copy;
    std::cout << "std::sort Time: " << measureTime(std::sort, arr) << " s\n";
 
    return 0;
}

 이 코드를 컴파일하고 실행한 후, 다양한 프로파일링 도구를 사용하여 각 정렬 알고리즘의 성능을 분석해보세요.

연습 문제

  1. gprof를 사용하여 위의 정렬 알고리즘 예제를 프로파일링하고, 각 함수의 실행 시간과 호출 횟수를 분석해보세요.
  2. Valgrind의 Cachegrind 도구를 사용하여 각 정렬 알고리즘의 캐시 성능을 비교해보세요. 어떤 알고리즘이 캐시 효율성이 가장 높은가요?
  3. perf를 사용하여 퀵 정렬 알고리즘의 분기 예측 실패율을 측정해보세요. 이를 개선할 수 있는 방법을 제안해보세요.
  4. 메모리 누수가 있는 프로그램을 작성하고, Valgrind의 Memcheck를 사용하여 이를 검출해보세요.
  5. 다음 함수의 성능을 프로파일링하고 최적화해보세요.
int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

 참고자료