C++ Benchmarking | chrono·Google Benchmark 성능 측정 완벽 정리

C++ Benchmarking | chrono·Google Benchmark 성능 측정 완벽 정리

이 글의 핵심

C++ 성능 벤치마킹: chrono 고해상도 시계로 측정하고 워밍업·반복 실행·통계 분석으로 신뢰할 수 있는 수치를 얻는 실무 절차를 설명합니다.

들어가며

벤치마킹(Benchmarking) 은 코드의 성능을 정량적으로 측정하는 과정입니다. 단순히 한 번 실행 시간을 재는 것이 아니라, 워밍업, 반복 실행, 통계 분석을 통해 신뢰할 수 있는 수치를 얻어야 합니다.

이 글을 읽으면

  • std::chrono로 고해상도 시간 측정을 구현합니다
  • 워밍업, 반복 실행, 통계 분석으로 정확한 벤치마크를 작성합니다
  • Google Benchmark 라이브러리로 전문적인 벤치마크를 구현합니다
  • 실무에서 자주 쓰이는 벤치마킹 패턴을 익힙니다

목차

  1. 기본 개념
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

기본 개념

벤치마킹 프로세스

graph LR
    A[코드 작성] --> B[워밍업]
    B --> C[측정 시작]
    C --> D[반복 실행]
    D --> E[측정 종료]
    E --> F[통계 분석]
    F --> G{목표 달성?}
    G -->|No| H[최적화]
    H --> A
    G -->|Yes| I[완료]

기본 측정

#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    
    // 코드 실행
    std::vector<int> v(1000000);
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(
        end - start
    );
    
    std::cout << "시간: " << duration.count() << "μs" << std::endl;
    
    return 0;
}

실전 구현

1) 기본 벤치마크 함수

#include <chrono>
#include <iostream>
#include <functional>

template<typename Func>
auto benchmark(Func f, int iterations = 1000) {
    using namespace std::chrono;
    
    auto start = high_resolution_clock::now();
    
    for (int i = 0; i < iterations; ++i) {
        f();
    }
    
    auto end = high_resolution_clock::now();
    auto total = duration_cast<microseconds>(end - start);
    
    return total.count() / iterations;
}

int main() {
    auto avgTime = benchmark([]() {
        std::vector<int> v(1000);
    });
    
    std::cout << "평균: " << avgTime << "μs" << std::endl;
    
    return 0;
}

2) 통계 수집

#include <vector>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <iostream>

class BenchmarkStats {
private:
    std::vector<double> samples;
    
public:
    void addSample(double microseconds) {
        samples.push_back(microseconds);
    }
    
    double mean() const {
        if (samples.empty()) return 0.0;
        auto sum = std::accumulate(samples.begin(), samples.end(), 0.0);
        return sum / samples.size();
    }
    
    double median() const {
        if (samples.empty()) return 0.0;
        auto sorted = samples;
        std::sort(sorted.begin(), sorted.end());
        return sorted[sorted.size() / 2];
    }
    
    double min() const {
        return *std::min_element(samples.begin(), samples.end());
    }
    
    double max() const {
        return *std::max_element(samples.begin(), samples.end());
    }
    
    double stddev() const {
        if (samples.size() < 2) return 0.0;
        double avg = mean();
        double variance = 0.0;
        for (double s : samples) {
            variance += (s - avg) * (s - avg);
        }
        return std::sqrt(variance / (samples.size() - 1));
    }
    
    double percentile(double p) const {
        if (samples.empty()) return 0.0;
        auto sorted = samples;
        std::sort(sorted.begin(), sorted.end());
        size_t index = static_cast<size_t>(p * sorted.size());
        return sorted[index];
    }
    
    void printStats() const {
        std::cout << "평균: " << mean() << "μs" << std::endl;
        std::cout << "중앙값: " << median() << "μs" << std::endl;
        std::cout << "최소: " << min() << "μs" << std::endl;
        std::cout << "최대: " << max() << "μs" << std::endl;
        std::cout << "표준편차: " << stddev() << "μs" << std::endl;
        std::cout << "P95: " << percentile(0.95) << "μs" << std::endl;
        std::cout << "P99: " << percentile(0.99) << "μs" << std::endl;
    }
};

3) 워밍업 패턴

#include <chrono>
#include <iostream>
#include <functional>

template<typename Func>
BenchmarkStats benchmarkWithWarmup(Func f, int warmup, int iterations) {
    // 워밍업
    for (int i = 0; i < warmup; ++i) {
        f();
    }
    
    // 측정
    BenchmarkStats stats;
    for (int i = 0; i < iterations; ++i) {
        auto start = std::chrono::high_resolution_clock::now();
        f();
        auto end = std::chrono::high_resolution_clock::now();
        
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(
            end - start
        );
        stats.addSample(duration.count());
    }
    
    return stats;
}

int main() {
    auto stats = benchmarkWithWarmup([]() {
        std::vector<int> v(1000);
    }, 10, 100);
    
    stats.printStats();
    
    return 0;
}

4) 알고리즘 비교

#include <algorithm>
#include <vector>
#include <chrono>
#include <iostream>
#include <random>

void compareSort() {
    std::vector<int> data(100000);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 100000);
    std::generate(data.begin(), data.end(), [&]() { return dis(gen); });
    
    // std::sort
    auto data1 = data;
    auto start1 = std::chrono::high_resolution_clock::now();
    std::sort(data1.begin(), data1.end());
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1);
    
    // std::stable_sort
    auto data2 = data;
    auto start2 = std::chrono::high_resolution_clock::now();
    std::stable_sort(data2.begin(), data2.end());
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2);
    
    std::cout << "sort: " << time1.count() << "ms" << std::endl;
    std::cout << "stable_sort: " << time2.count() << "ms" << std::endl;
}

int main() {
    compareSort();
    
    return 0;
}

출력 예시:

sort: 8ms
stable_sort: 12ms

5) Google Benchmark

설치

# Linux/macOS
git clone https://github.com/google/benchmark.git
cd benchmark
cmake -E make_directory "build"
cmake -E chdir "build" cmake -DBENCHMARK_DOWNLOAD_DEPENDENCIES=on -DCMAKE_BUILD_TYPE=Release ../
cmake --build "build" --config Release
sudo cmake --build "build" --target install

기본 사용

#include <benchmark/benchmark.h>
#include <vector>

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
    }
}

BENCHMARK(BM_VectorPushBack)->Range(8, 8<<10);

static void BM_VectorReserve(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(state.range(0));
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
    }
}

BENCHMARK(BM_VectorReserve)->Range(8, 8<<10);

BENCHMARK_MAIN();

컴파일 및 실행:

g++ -std=c++17 bench.cpp -lbenchmark -lpthread -o bench
./bench

출력 예시:

-----------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
-----------------------------------------------------------------
BM_VectorPushBack/8           120 ns          120 ns      5600000
BM_VectorPushBack/64          850 ns          850 ns       800000
BM_VectorPushBack/512        6800 ns         6800 ns       100000
BM_VectorReserve/8             80 ns           80 ns      8700000
BM_VectorReserve/64           500 ns          500 ns      1400000
BM_VectorReserve/512         4000 ns         4000 ns       175000

고급 활용

1) 최적화 방지

#include <benchmark/benchmark.h>

static void BM_Compute(benchmark::State& state) {
    for (auto _ : state) {
        int result = 1 + 1;
        benchmark::DoNotOptimize(result);  // 최적화 방지
    }
}

BENCHMARK(BM_Compute);

BENCHMARK_MAIN();

2) 메모리 사용량 측정

#include <benchmark/benchmark.h>
#include <vector>

static void BM_VectorMemory(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v(state.range(0));
        benchmark::DoNotOptimize(v.data());
        
        state.SetBytesProcessed(state.iterations() * state.range(0) * sizeof(int));
    }
}

BENCHMARK(BM_VectorMemory)->Range(8, 8<<10);

BENCHMARK_MAIN();

3) 사용자 정의 카운터

#include <benchmark/benchmark.h>
#include <vector>

static void BM_CustomCounter(benchmark::State& state) {
    int operations = 0;
    
    for (auto _ : state) {
        std::vector<int> v(state.range(0));
        operations += state.range(0);
    }
    
    state.counters["operations"] = operations;
}

BENCHMARK(BM_CustomCounter)->Range(8, 8<<10);

BENCHMARK_MAIN();

성능 비교

정렬 알고리즘 비교

테스트: 100,000개 요소

알고리즘평균 시간최악 시간 복잡도안정성메모리
std::sort~8msO(N log N)O(log N)
std::stable_sort~12msO(N log² N)O(N)
std::partial_sort~5ms (상위 10%)O(N log K)O(1)
std::nth_element~2ms (중앙값)O(N)O(1)

컨테이너 삽입 비교

테스트: 10,000개 요소 삽입

방법시간배속
vector (reserve 없음)150us1x
vector (reserve 있음)50us3x
list200us0.75x
deque100us1.5x

결론: reserve3배 개선


실무 사례

사례 1: 해시 함수 비교

#include <benchmark/benchmark.h>
#include <string>
#include <functional>

static void BM_StdHash(benchmark::State& state) {
    std::string str = "hello world";
    std::hash<std::string> hasher;
    
    for (auto _ : state) {
        size_t hash = hasher(str);
        benchmark::DoNotOptimize(hash);
    }
}

BENCHMARK(BM_StdHash);

static void BM_CustomHash(benchmark::State& state) {
    std::string str = "hello world";
    
    for (auto _ : state) {
        size_t hash = 0;
        for (char c : str) {
            hash = hash * 31 + c;
        }
        benchmark::DoNotOptimize(hash);
    }
}

BENCHMARK(BM_CustomHash);

BENCHMARK_MAIN();

사례 2: JSON 파싱 성능

#include <benchmark/benchmark.h>
#include <nlohmann/json.hpp>
#include <string>

static void BM_JsonParse(benchmark::State& state) {
    std::string json_str = R"({"name":"John","age":30,"city":"New York"})";
    
    for (auto _ : state) {
        auto json = nlohmann::json::parse(json_str);
        benchmark::DoNotOptimize(json);
    }
}

BENCHMARK(BM_JsonParse);

static void BM_JsonSerialize(benchmark::State& state) {
    nlohmann::json json = {{"name", "John"}, {"age", 30}, {"city", "New York"}};
    
    for (auto _ : state) {
        std::string str = json.dump();
        benchmark::DoNotOptimize(str);
    }
}

BENCHMARK(BM_JsonSerialize);

BENCHMARK_MAIN();

사례 3: 메모리 할당 전략

#include <benchmark/benchmark.h>
#include <vector>
#include <memory>

static void BM_RawPointer(benchmark::State& state) {
    for (auto _ : state) {
        int* ptr = new int[state.range(0)];
        benchmark::DoNotOptimize(ptr);
        delete[] ptr;
    }
}

BENCHMARK(BM_RawPointer)->Range(8, 8<<10);

static void BM_UniquePtr(benchmark::State& state) {
    for (auto _ : state) {
        auto ptr = std::make_unique<int[]>(state.range(0));
        benchmark::DoNotOptimize(ptr.get());
    }
}

BENCHMARK(BM_UniquePtr)->Range(8, 8<<10);

static void BM_Vector(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v(state.range(0));
        benchmark::DoNotOptimize(v.data());
    }
}

BENCHMARK(BM_Vector)->Range(8, 8<<10);

BENCHMARK_MAIN();

트러블슈팅

문제 1: 컴파일러 최적화로 코드 제거

증상: 벤치마크 시간이 0에 가까움

// ❌ 최적화로 제거
static void BM_Bad(benchmark::State& state) {
    for (auto _ : state) {
        int x = 42;  // 컴파일러가 제거
    }
}

// ✅ 최적화 방지
static void BM_Good(benchmark::State& state) {
    for (auto _ : state) {
        int x = 42;
        benchmark::DoNotOptimize(x);  // 최적화 방지
    }
}

문제 2: 캐시 효과

증상: 첫 실행이 느리고 이후 빨라짐

// ❌ 캐시 효과
auto time1 = benchmark(f);  // 캐시 미스 (느림)
auto time2 = benchmark(f);  // 캐시 히트 (빠름)

// ✅ 워밍업
for (int i = 0; i < 10; ++i) f();
auto time = benchmark(f);

문제 3: 측정 오버헤드

증상: 매우 짧은 작업의 측정 시간이 부정확

// ❌ 측정 오버헤드 > 실제 시간
auto time = benchmark([]() {
    int x = 1 + 1;  // 너무 짧음
});

// ✅ 여러 번 반복 후 평균
auto time = benchmark([]() {
    for (int i = 0; i < 1000; ++i) {
        int x = 1 + 1;
    }
}, 100) / 1000;

문제 4: 백그라운드 프로세스

증상: 측정 시간이 불안정

// ❌ 다른 프로세스 실행 중
// 브라우저, IDE, 백그라운드 서비스 등

// ✅ 격리 실행
// 1. 다른 프로세스 종료
// 2. CPU 고정 (Linux)
taskset -c 0 ./bench

// 3. 우선순위 높이기
nice -n -20 ./bench

마무리

C++ 벤치마킹성능 최적화의 핵심 도구입니다.

핵심 요약

  1. 기본 측정

    • std::chrono::high_resolution_clock
    • duration_cast<microseconds>
  2. 정확한 측정

    • 워밍업 (10-100회)
    • 반복 실행 (100-1000회)
    • 통계 분석 (평균, 중앙값, 표준편차)
  3. 최적화 방지

    • volatile 또는 benchmark::DoNotOptimize
    • 결과 사용
  4. Google Benchmark

    • 전문적인 벤치마크 프레임워크
    • 자동 반복, 통계, 비교

벤치마킹 체크리스트

항목권장 사항이유
워밍업10-100회 실행CPU 캐시, 분기 예측 최적화
반복 횟수100-1000회통계적 유의성 확보
최적화 방지volatile 또는 DoNotOptimize컴파일러 최적화 제거 방지
격리 실행다른 프로세스 종료노이즈 최소화
CPU 고정taskset (Linux)코어 이동 방지
릴리즈 빌드-O3 -DNDEBUG실제 성능 측정

통계 지표

지표의미활용
평균 (Mean)전체 평균 시간일반적인 성능
중앙값 (Median)중간 값이상치 제거
최소 (Min)최고 성능최적 조건
최대 (Max)최악 성능최악 케이스
표준편차 (StdDev)변동성안정성 평가
백분위수 (P95, P99)상위 5%, 1%SLA 기준

코드 예제 치트시트

// 기본 측정
auto start = std::chrono::high_resolution_clock::now();
// 코드 실행
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

// 워밍업 + 반복
for (int i = 0; i < 10; ++i) f();  // 워밍업
BenchmarkStats stats;
for (int i = 0; i < 100; ++i) {
    auto time = benchmark(f);
    stats.addSample(time);
}
stats.printStats();

// Google Benchmark
static void BM_Function(benchmark::State& state) {
    for (auto _ : state) {
        // 코드 실행
    }
}
BENCHMARK(BM_Function);
BENCHMARK_MAIN();

다음 단계

  • 스톱워치: C++ 스톱워치와 벤치마크
  • 성능 최적화: C++ 성능 최적화
  • 캐시 최적화: C++ 캐시 최적화

참고 자료

한 줄 정리: 벤치마킹은 워밍업, 반복 실행, 통계 분석으로 신뢰할 수 있는 성능 수치를 얻고, Google Benchmark로 전문적인 측정을 자동화한다.