C에서 Google Benchmark를 활용한 성능 측정 및 최적화

Google Benchmark는 Google에서 개발한 오픈소스 벤치마킹 라이브러리로, C 및 C++에서 성능을 측정하고 분석하는 데 유용합니다. 이 라이브러리는 마이크로벤치마킹(microbenchmarking)에 최적화되어 있으며, 함수나 알고리즘의 실행 시간을 정확하게 측정할 수 있도록 다양한 기능을 제공합니다.

일반적으로 성능 측정을 위해 clock() 함수나 gettimeofday() 같은 타이머 함수를 사용할 수 있지만, 이러한 방법은 정밀도가 낮거나 오버헤드가 발생할 수 있습니다. Google Benchmark는 보다 정교한 시간 측정 기법을 사용하여 보다 신뢰성 있는 결과를 제공하며, 실행 반복 횟수를 조절하거나 입력 크기에 따른 성능 변화를 자동으로 분석하는 기능을 갖추고 있습니다.

본 기사에서는 Google Benchmark를 C 환경에서 활용하는 방법을 소개하고, 이를 이용해 프로그램의 성능을 평가하는 다양한 기법을 설명합니다. 또한 실제 코드를 통해 벤치마킹을 수행하는 과정과 최적화 적용 후 성능 개선 여부를 비교하는 방법도 다룹니다. 이를 통해 C 개발자가 성능 병목을 찾아내고 최적화할 수 있도록 실질적인 가이드를 제공합니다.

Google Benchmark란 무엇인가

Google Benchmark는 Google이 개발한 오픈소스 성능 측정 라이브러리로, C 및 C++에서 함수나 알고리즘의 실행 시간을 정확하게 측정할 수 있도록 설계되었습니다.

주요 특징

  1. 마이크로벤치마킹 지원
  • 개별 함수, 알고리즘 또는 특정 코드 블록의 실행 시간을 측정할 수 있습니다.
  • 일반적인 clock() 함수나 gettimeofday() 같은 방법보다 정밀한 측정을 제공합니다.
  1. 자동 반복 실행
  • 벤치마크 실행 횟수를 자동으로 조절하여 보다 정확한 평균 실행 시간을 제공합니다.
  • 실행 시간이 짧은 경우, 충분한 반복 실행 후 결과를 도출합니다.
  1. 입력 크기별 성능 측정
  • 다양한 입력 크기에 따른 성능 변화를 분석할 수 있습니다.
  • 벤치마크를 실행하면서 입력 크기를 동적으로 변경할 수 있습니다.
  1. 메모리 사용량 측정 지원
  • 특정 함수나 알고리즘이 사용하는 메모리 크기를 측정할 수 있습니다.
  • 성능뿐만 아니라 메모리 효율성 분석에도 활용할 수 있습니다.
  1. JSON 및 CSV 형식 결과 출력
  • 벤치마크 결과를 JSON 또는 CSV 형식으로 저장할 수 있어 자동 분석 및 시각화에 유용합니다.

기존 벤치마킹 기법과 비교

방법정밀도자동 반복 실행입력 크기 조정메모리 사용량 측정
clock() 함수낮음XXX
gettimeofday()중간XXX
Google Benchmark높음OOO

Google Benchmark는 성능을 보다 정밀하게 측정할 수 있도록 다양한 기능을 제공하며, 이를 통해 C 프로그램의 병목을 쉽게 찾아 최적화할 수 있도록 도와줍니다.

Google Benchmark 설치 및 설정

Google Benchmark를 C 환경에서 사용하려면 먼저 라이브러리를 설치하고 프로젝트에 포함해야 합니다. 아래에서는 Ubuntu 및 Windows 환경에서 설치하는 방법과 CMake를 활용한 설정 방법을 설명합니다.


1. Google Benchmark 설치

(1) Ubuntu 및 Linux 환경에서 설치

Google Benchmark는 소스에서 빌드하여 사용할 수 있으며, CMake를 활용해 설치할 수 있습니다.

# 필수 패키지 설치
sudo apt update
sudo apt install -y cmake g++ git

# Google Benchmark 소스 코드 다운로드
git clone https://github.com/google/benchmark.git
cd benchmark

# 서브모듈 업데이트 (Google Test 포함)
git submodule update --init --recursive

# 빌드 및 설치
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
(2) Windows 환경에서 설치

Windows에서는 Microsoft Visual Studio와 CMake를 사용하여 빌드할 수 있습니다.

  1. Git을 사용해 소스를 다운로드합니다.
  2. Visual Studio에서 x64 Native Tools Command Prompt를 실행합니다.
  3. CMake를 사용하여 프로젝트를 구성하고 빌드합니다.
git clone https://github.com/google/benchmark.git
cd benchmark
git submodule update --init --recursive
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release

빌드가 완료되면, benchmark.lib(Windows) 또는 libbenchmark.a(Linux) 파일이 생성됩니다.


2. 프로젝트에서 Google Benchmark 사용 설정

(1) CMake를 이용한 프로젝트 설정

Google Benchmark를 프로젝트에서 사용하려면 CMakeLists.txt 파일에 다음과 같이 추가합니다.

cmake_minimum_required(VERSION 3.10)
project(MyBenchmarkProject)

# Google Benchmark 라이브러리 포함
find_package(benchmark REQUIRED)

add_executable(my_benchmark main.c)
target_link_libraries(my_benchmark benchmark::benchmark)
(2) Makefile을 이용한 수동 설정

CMake를 사용하지 않는 경우, 직접 라이브러리를 링크할 수도 있습니다.

CC=gcc
CFLAGS=-O2 -std=c11
LDFLAGS=-lbenchmark -lpthread

all: my_benchmark

my_benchmark: main.c
    $(CC) $(CFLAGS) main.c -o my_benchmark $(LDFLAGS)

clean:
    rm -f my_benchmark

설치 및 설정이 완료되면 Google Benchmark를 활용한 벤치마킹 코드를 작성할 준비가 완료됩니다. 다음 섹션에서는 기본적인 벤치마킹 코드 작성 방법을 살펴봅니다.

기본적인 벤치마킹 코드 작성

Google Benchmark를 활용하여 C에서 간단한 성능 벤치마킹 코드를 작성하는 방법을 설명합니다. 기본적인 벤치마킹 코드는 실행 시간 측정을 위한 테스트 함수를 작성하고, 이를 BENCHMARK() 매크로로 등록하여 실행하는 방식으로 구성됩니다.


1. Google Benchmark 헤더 포함

Google Benchmark를 사용하려면 benchmark/benchmark.h 헤더를 포함해야 합니다.

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

2. 간단한 성능 벤치마킹 함수

예제에서는 배열의 덧셈 연산을 수행하는 간단한 벤치마킹을 수행합니다.

static void BM_ArrayAddition(benchmark::State& state) {
    int size = state.range(0);  // 입력 크기
    int* array = (int*)malloc(size * sizeof(int));

    for (int i = 0; i < size; i++) {
        array[i] = i;
    }

    for (auto _ : state) {
        int sum = 0;
        for (int i = 0; i < size; i++) {
            sum += array[i];
        }
        benchmark::DoNotOptimize(sum);  // 최적화 방지
    }

    free(array);
}

// 벤치마킹 범위 설정
BENCHMARK(BM_ArrayAddition)->Arg(1000)->Arg(10000)->Arg(100000);

위 코드에서 BM_ArrayAddition() 함수는 크기가 state.range(0)인 배열을 생성한 후, 모든 원소를 합산하는 연산을 수행합니다. benchmark::DoNotOptimize(sum);은 컴파일러 최적화로 인해 불필요한 연산이 제거되는 것을 방지하기 위해 사용됩니다.


3. 벤치마킹 실행 코드

Google Benchmark의 BENCHMARK_MAIN() 매크로를 사용하면, 자동으로 벤치마크 테스트가 실행됩니다.

BENCHMARK_MAIN();

4. 실행 방법

프로그램을 컴파일한 후 실행하면, Google Benchmark가 자동으로 반복 실행하여 평균 실행 시간을 출력합니다.

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                   Time       CPU Iterations
--------------------------------------------------------------
BM_ArrayAddition/1000       3.4 ns     3.4 ns  200000000
BM_ArrayAddition/10000      15.6 ns    15.6 ns  50000000
BM_ArrayAddition/100000     120.8 ns   120.8 ns 5000000

이 결과는 입력 크기에 따라 실행 시간이 증가하는 모습을 보여줍니다.


정리

  • BENCHMARK()를 사용하여 벤치마크 대상을 등록합니다.
  • state.range(0)을 사용하여 입력 크기별 테스트를 수행할 수 있습니다.
  • benchmark::DoNotOptimize()를 사용하여 최적화 방지 처리합니다.
  • BENCHMARK_MAIN()을 사용하여 벤치마크를 실행합니다.

다음 섹션에서는 입력 크기에 따른 성능 변화를 분석하는 방법을 살펴봅니다.

입력 크기에 따른 성능 분석

Google Benchmark를 활용하면 입력 크기에 따라 성능이 어떻게 변화하는지를 쉽게 분석할 수 있습니다. 이 과정은 알고리즘의 시간 복잡도를 평가하거나 특정 코드가 대량의 데이터를 처리할 때 성능이 어떻게 변화하는지를 측정하는 데 유용합니다.


1. 입력 크기 변화에 따른 실행 시간 측정

Google Benchmark의 RangeMultiplier() 함수를 사용하면 입력 크기를 단계적으로 증가시키면서 성능을 측정할 수 있습니다.

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

// 배열을 생성하고 모든 요소를 더하는 함수
static void BM_ArraySum(benchmark::State& state) {
    int size = state.range(0);  // 현재 입력 크기
    int* array = (int*)malloc(size * sizeof(int));

    for (int i = 0; i < size; i++) {
        array[i] = i;
    }

    for (auto _ : state) {
        int sum = 0;
        for (int i = 0; i < size; i++) {
            sum += array[i];
        }
        benchmark::DoNotOptimize(sum);  // 최적화 방지
    }

    free(array);
}

// 입력 크기를 점진적으로 증가시키며 실행
BENCHMARK(BM_ArraySum)->RangeMultiplier(10)->Range(100, 1000000);
BENCHMARK_MAIN();

위 코드에서 RangeMultiplier(10)->Range(100, 1000000) 설정은 입력 크기를 100부터 시작하여 10배씩 증가시키면서 테스트를 수행하도록 합니다.


2. 실행 결과 예시

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                   Time       CPU Iterations
--------------------------------------------------------------
BM_ArraySum/100             5.2 ns     5.2 ns  100000000
BM_ArraySum/1000            20.1 ns    20.1 ns 50000000
BM_ArraySum/10000           150.6 ns   150.6 ns 5000000
BM_ArraySum/100000          1.4 µs     1.4 µs  1000000
BM_ArraySum/1000000         12.8 µs    12.8 µs 500000

이 결과에서 입력 크기가 증가함에 따라 실행 시간이 선형적으로 증가하는 모습을 확인할 수 있습니다. 이는 배열 합산 연산이 O(n)의 시간 복잡도를 가진다는 점을 실험적으로 증명하는 과정입니다.


3. 로그 스케일 그래프 활용

Google Benchmark는 JSON 또는 CSV 형식으로 데이터를 출력할 수 있어, 이를 활용해 그래프로 시각화할 수도 있습니다.

./my_benchmark --benchmark_format=json > benchmark_result.json

JSON 데이터를 Python, Excel 등에서 그래프로 변환하면 입력 크기와 실행 시간 간의 관계를 더욱 직관적으로 분석할 수 있습니다.


정리

  • RangeMultiplier()Range()를 활용해 입력 크기에 따른 성능 변화를 측정할 수 있습니다.
  • 결과를 로그 스케일로 분석하면 O(n), O(n²) 등의 성능 패턴을 쉽게 파악할 수 있습니다.
  • benchmark_format=json 옵션을 사용하면 실행 결과를 JSON으로 저장하여 추가 분석이 가능합니다.

다음 섹션에서는 반복 실행과 평균 시간 계산을 최적화하는 방법을 살펴보겠습니다.

반복 실행과 평균 시간 계산

Google Benchmark는 실행 시간을 보다 정확하게 측정하기 위해 동일한 테스트를 여러 번 반복 실행하고, 평균 시간을 계산하는 기능을 제공합니다. 이를 통해 벤치마킹 결과의 신뢰성을 높이고, 불안정한 성능 변화를 방지할 수 있습니다.


1. 기본 반복 실행 구조

Google Benchmark는 기본적으로 벤치마크를 여러 번 반복 실행한 후 평균 실행 시간을 계산합니다. 이 반복 실행은 state 객체를 통해 자동으로 조정됩니다.

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

// 간단한 덧셈 연산 벤치마크
static void BM_SimpleAddition(benchmark::State& state) {
    int a = 5, b = 10;
    for (auto _ : state) {
        int result = a + b;
        benchmark::DoNotOptimize(result);  // 최적화 방지
    }
}

// 벤치마크 등록
BENCHMARK(BM_SimpleAddition);
BENCHMARK_MAIN();

위 코드에서 for (auto _ : state) 구문은 Google Benchmark가 자동으로 적절한 반복 횟수를 결정하도록 합니다.


2. 수동으로 반복 횟수 조정

Google Benchmark는 기본적으로 적절한 반복 횟수를 자동으로 조정하지만, 특정 반복 횟수를 강제할 수도 있습니다.

BENCHMARK(BM_SimpleAddition)->Iterations(1000000);

위 코드에서는 동일한 테스트를 100만 번 반복 실행하여 평균 시간을 계산하도록 설정합니다.


3. 실행 시간 기준으로 반복 횟수 설정

테스트 실행 시간을 기준으로 반복 횟수를 설정할 수도 있습니다. 예를 들어, 최소 1초 동안 반복 실행하도록 설정하면, 테스트의 안정성이 높아집니다.

BENCHMARK(BM_SimpleAddition)->MinTime(1.0);

이 설정은 실행 시간이 최소 1초가 될 때까지 반복 실행하며, 짧은 실행 시간이 측정될 때 발생하는 오차를 줄여줍니다.


4. 실행 결과 예시

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                   Time       CPU Iterations
--------------------------------------------------------------
BM_SimpleAddition           0.5 ns     0.5 ns  1000000000
  • Time: 벤치마크 함수 실행 시간
  • CPU: CPU 사용 시간
  • Iterations: 실행 횟수

위 결과에서 0.5 ns는 평균 실행 시간을 의미하며, 총 10억 번 실행되었음을 알 수 있습니다.


정리

  • state 객체를 이용해 자동 반복 실행을 수행합니다.
  • Iterations(N)을 설정하여 실행 횟수를 강제할 수 있습니다.
  • MinTime(T)을 사용하면 최소 실행 시간을 설정할 수 있습니다.
  • 반복 실행을 통해 측정 오차를 줄이고, 보다 신뢰성 높은 벤치마킹 결과를 얻을 수 있습니다.

다음 섹션에서는 메모리 사용량 측정 방법을 살펴보겠습니다.

메모리 사용량 측정

Google Benchmark는 코드 실행 성능뿐만 아니라 메모리 사용량도 측정할 수 있습니다. 이를 통해 특정 연산이 얼마나 많은 메모리를 소비하는지 분석하고, 최적화를 진행할 수 있습니다.


1. 메모리 사용량 측정을 위한 설정

Google Benchmark에서 메모리 사용량을 측정하려면 SetBytesProcessed() 또는 SetComplexityN() 함수를 사용해야 합니다.

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

// 배열 할당 및 초기화 벤치마크
static void BM_MemoryAllocation(benchmark::State& state) {
    size_t size = state.range(0);  // 입력 크기
    for (auto _ : state) {
        int* array = (int*)malloc(size * sizeof(int));
        for (size_t i = 0; i < size; i++) {
            array[i] = i;
        }
        benchmark::DoNotOptimize(array);
        free(array);
    }
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// 입력 크기를 1KB에서 1MB까지 증가시키며 실행
BENCHMARK(BM_MemoryAllocation)->RangeMultiplier(2)->Range(1024, 1024 * 1024);
BENCHMARK_MAIN();

2. 실행 결과 예시

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                     Time      CPU Iterations  Memory
--------------------------------------------------------------
BM_MemoryAllocation/1024       2.5 ns    2.5 ns  100000000  1.0 KB
BM_MemoryAllocation/2048       4.9 ns    4.9 ns  50000000   2.0 KB
BM_MemoryAllocation/4096       9.7 ns    9.7 ns  25000000   4.0 KB
BM_MemoryAllocation/8192       19.2 ns   19.2 ns  12500000  8.0 KB
BM_MemoryAllocation/16384      38.3 ns   38.3 ns  6250000   16.0 KB
  • Time: 함수 실행 시간
  • CPU: CPU 사용 시간
  • Iterations: 실행 횟수
  • Memory: 메모리 사용량

위 결과에서 입력 크기가 증가할수록 메모리 소비량도 증가하는 것을 확인할 수 있습니다.


3. SetBytesProcessed()SetComplexityN() 차이점

함수설명
SetBytesProcessed()벤치마크 실행 중 처리된 총 바이트 수를 설정 (메모리 사용량 측정 가능)
SetComplexityN()실행 시간 복잡도를 측정하여 O(n), O(n²) 등의 패턴을 분석

정리

  • SetBytesProcessed()를 사용하면 메모리 사용량을 함께 측정할 수 있습니다.
  • RangeMultiplier()를 사용하여 입력 크기별 성능 변화를 분석할 수 있습니다.
  • 실행 결과에서 메모리 사용량을 확인하고 최적화를 진행할 수 있습니다.

다음 섹션에서는 최적화 기법 적용 및 성능 비교를 다룹니다.

최적화 기법 적용 및 성능 비교

Google Benchmark를 활용하면 코드의 성능을 정량적으로 측정하고, 최적화 기법 적용 전후의 성능을 비교할 수 있습니다. 여기서는 메모리 할당 방식 변경, 반복문 최적화, 캐시 친화적 코드 작성 등의 기법을 적용하여 성능을 개선하는 방법을 살펴봅니다.


1. 최적화 전후 성능 비교 코드

아래 예제에서는 동적 메모리 할당 방식 변경캐시 최적화 기법을 적용한 코드의 성능을 비교합니다.

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

// (1) 기본적인 동적 메모리 할당과 초기화
static void BM_DefaultAllocation(benchmark::State& state) {
    size_t size = state.range(0);
    for (auto _ : state) {
        int* array = (int*)malloc(size * sizeof(int));
        for (size_t i = 0; i < size; i++) {
            array[i] = i;
        }
        benchmark::DoNotOptimize(array);
        free(array);
    }
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// (2) 메모리 할당 최적화: `calloc()`을 사용하여 초기화 포함
static void BM_CallocOptimization(benchmark::State& state) {
    size_t size = state.range(0);
    for (auto _ : state) {
        int* array = (int*)calloc(size, sizeof(int));  // 초기화 포함
        benchmark::DoNotOptimize(array);
        free(array);
    }
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// (3) 캐시 친화적 순차 접근 최적화
static void BM_CacheFriendly(benchmark::State& state) {
    size_t size = state.range(0);
    int* array = (int*)malloc(size * sizeof(int));
    for (auto _ : state) {
        for (size_t i = 0; i < size; i += 16) {  // 16개 단위로 접근하여 캐시 활용
            array[i] = i;
        }
        benchmark::DoNotOptimize(array);
    }
    free(array);
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// 입력 크기를 1KB ~ 1MB 범위로 설정
BENCHMARK(BM_DefaultAllocation)->RangeMultiplier(10)->Range(1024, 1024 * 1024);
BENCHMARK(BM_CallocOptimization)->RangeMultiplier(10)->Range(1024, 1024 * 1024);
BENCHMARK(BM_CacheFriendly)->RangeMultiplier(10)->Range(1024, 1024 * 1024);

BENCHMARK_MAIN();

2. 실행 결과 예시

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                        Time     CPU  Iterations  Memory
--------------------------------------------------------------
BM_DefaultAllocation/1024        5.5 ns   5.5 ns  100000000  1.0 KB
BM_CallocOptimization/1024       4.0 ns   4.0 ns  100000000  1.0 KB
BM_CacheFriendly/1024            2.2 ns   2.2 ns  100000000  1.0 KB
BM_DefaultAllocation/10240       35.1 ns  35.1 ns  50000000  10.0 KB
BM_CallocOptimization/10240      25.0 ns  25.0 ns  50000000  10.0 KB
BM_CacheFriendly/10240           12.5 ns  12.5 ns  50000000  10.0 KB
BM_DefaultAllocation/102400      310.5 ns 310.5 ns 1000000  100.0 KB
BM_CallocOptimization/102400     270.3 ns 270.3 ns 1000000  100.0 KB
BM_CacheFriendly/102400          150.2 ns 150.2 ns 1000000  100.0 KB

해석:

  • calloc()을 사용한 메모리 할당(BM_CallocOptimization)은 malloc() + for 초기화보다 10~20% 더 빠름
  • 캐시 친화적 접근(BM_CacheFriendly)은 최대 2배 이상 성능 향상
  • 데이터 크기가 커질수록 캐시를 고려한 최적화가 중요함

3. 최적화 적용 시 고려할 사항

  • 메모리 초기화 방식 최적화
  • calloc()은 OS에서 초기화된 메모리를 직접 할당받기 때문에 속도가 빠를 수 있음.
  • 반면 malloc()은 초기화되지 않은 메모리를 할당하고, 수동으로 for 루프를 돌려 초기화해야 함.
  • 캐시 친화적인 데이터 접근
  • CPU 캐시는 메모리보다 속도가 훨씬 빠르므로, 순차적인 메모리 접근 방식이 성능을 높이는 핵심 전략이 될 수 있음.
  • 일반적인 루프 대신, 16바이트(캐시 라인 크기) 단위로 접근하면 성능이 향상될 가능성이 높음.
  • 메모리 할당 최적화
  • 작은 크기의 객체를 여러 개 할당할 경우, malloc()보다는 메모리 풀(Pool) 할당 방식이 성능에 더 유리함.
  • 메모리 할당이 많은 경우, jemalloc 또는 tcmalloc 같은 고급 메모리 할당기를 사용하는 것도 고려할 수 있음.

정리

  • Google Benchmark를 활용해 최적화 전후의 성능을 정량적으로 비교할 수 있다.
  • calloc()을 활용하면 malloc() + for 초기화보다 최대 20% 빠른 성능을 보일 수 있다.
  • CPU 캐시 친화적인 코드(순차 접근)를 적용하면 최대 2배 이상 성능 향상이 가능하다.
  • 입력 크기가 클수록 메모리 할당 및 데이터 접근 방식이 성능에 미치는 영향이 커진다.

다음 섹션에서는 실전 예제: 정렬 알고리즘 성능 비교를 다룹니다.

실전 예제: 정렬 알고리즘 비교

Google Benchmark를 활용하여 Quick SortMerge Sort의 성능을 비교해 보겠습니다. 두 알고리즘의 실행 시간을 벤치마킹하여 입력 크기에 따른 성능 차이를 분석합니다.


1. Quick Sort와 Merge Sort 구현

C에서 벤치마킹을 수행하기 위해 두 가지 정렬 알고리즘을 직접 구현합니다.

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

// Quick Sort 구현
void quick_sort(int* arr, int left, int right) {
    if (left >= right) return;

    int pivot = arr[right];  
    int i = left - 1;

    for (int j = left; j < right; j++) {
        if (arr[j] < pivot) {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }

    int temp = arr[i + 1];
    arr[i + 1] = arr[right];
    arr[right] = temp;

    quick_sort(arr, left, i);
    quick_sort(arr, i + 2, right);
}

// Merge Sort 구현
void merge(int* arr, int left, int mid, int right) {
    int left_size = mid - left + 1;
    int right_size = right - mid;

    int* left_arr = (int*)malloc(left_size * sizeof(int));
    int* right_arr = (int*)malloc(right_size * sizeof(int));

    memcpy(left_arr, &arr[left], left_size * sizeof(int));
    memcpy(right_arr, &arr[mid + 1], right_size * sizeof(int));

    int i = 0, j = 0, k = left;
    while (i < left_size && j < right_size) {
        if (left_arr[i] <= right_arr[j]) {
            arr[k++] = left_arr[i++];
        } else {
            arr[k++] = right_arr[j++];
        }
    }

    while (i < left_size) arr[k++] = left_arr[i++];
    while (j < right_size) arr[k++] = right_arr[j++];

    free(left_arr);
    free(right_arr);
}

void merge_sort(int* arr, int left, int right) {
    if (left >= right) return;

    int mid = left + (right - left) / 2;
    merge_sort(arr, left, mid);
    merge_sort(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

2. Google Benchmark를 이용한 성능 측정

정렬 알고리즘을 벤치마킹하기 위해 BM_QuickSortBM_MergeSort 벤치마크 함수를 작성합니다.

// Quick Sort 벤치마킹
static void BM_QuickSort(benchmark::State& state) {
    size_t size = state.range(0);
    int* arr = (int*)malloc(size * sizeof(int));

    for (auto _ : state) {
        for (size_t i = 0; i < size; i++) arr[i] = rand() % 100000;
        quick_sort(arr, 0, size - 1);
        benchmark::DoNotOptimize(arr);
    }

    free(arr);
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// Merge Sort 벤치마킹
static void BM_MergeSort(benchmark::State& state) {
    size_t size = state.range(0);
    int* arr = (int*)malloc(size * sizeof(int));

    for (auto _ : state) {
        for (size_t i = 0; i < size; i++) arr[i] = rand() % 100000;
        merge_sort(arr, 0, size - 1);
        benchmark::DoNotOptimize(arr);
    }

    free(arr);
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(size * sizeof(int)));
}

// 입력 크기를 1K ~ 1M 범위로 설정
BENCHMARK(BM_QuickSort)->RangeMultiplier(10)->Range(1024, 1024 * 1024);
BENCHMARK(BM_MergeSort)->RangeMultiplier(10)->Range(1024, 1024 * 1024);

BENCHMARK_MAIN();

3. 실행 결과 예시

./my_benchmark

출력 예시:

--------------------------------------------------------------
Benchmark                  Time      CPU  Iterations  Memory
--------------------------------------------------------------
BM_QuickSort/1024          20.1 ns   20.1 ns  50000000   4.0 KB
BM_MergeSort/1024          30.3 ns   30.3 ns  50000000   4.0 KB
BM_QuickSort/10240         120.4 ns  120.4 ns 5000000    40.0 KB
BM_MergeSort/10240         180.6 ns  180.6 ns 5000000    40.0 KB
BM_QuickSort/102400        1.1 µs    1.1 µs  500000     400.0 KB
BM_MergeSort/102400        2.5 µs    2.5 µs  500000     400.0 KB
BM_QuickSort/1024000       12.2 µs   12.2 µs  100000     4.0 MB
BM_MergeSort/1024000       23.7 µs   23.7 µs  100000     4.0 MB

해석:

  • Quick Sort(BM_QuickSort)가 Merge Sort(BM_MergeSort)보다 전반적으로 빠름
  • Quick Sort의 평균 실행 시간은 O(n log n), Merge Sort도 O(n log n)이지만, 재귀 호출 및 추가 메모리 사용으로 인해 시간이 더 소요됨
  • 입력 크기가 커질수록 Merge Sort는 추가적인 메모리 할당 비용이 발생하여 속도 저하가 심해짐

4. 성능 분석 및 최적화 전략

정렬 알고리즘시간 복잡도공간 복잡도특징
Quick SortO(n log n)O(log n)일반적으로 빠르며, 추가 메모리 사용 적음
Merge SortO(n log n)O(n)안정 정렬(Stable Sort), 대용량 데이터에 적합

최적화 방향:

  • Quick Sort는 피벗 선택 최적화를 통해 평균적인 성능을 개선할 수 있음
  • Merge Sort는 메모리 복사 최소화 기법을 적용하여 메모리 사용량을 줄일 수 있음

정리

  • Google Benchmark를 활용하면 정렬 알고리즘 간 성능 비교가 가능하다.
  • Quick Sort는 Merge Sort보다 메모리 사용이 적고 빠른 경향을 보인다.
  • Merge Sort는 추가 메모리를 사용하지만 안정 정렬(stable sort)의 특징을 가진다.
  • 벤치마킹을 통해 입력 크기에 따른 성능 차이를 분석하고 최적화를 진행할 수 있다.

다음 섹션에서는 전체 요약을 진행하겠습니다.

요약

본 기사에서는 Google Benchmark를 활용하여 C 프로그램의 성능을 측정하고 최적화하는 방법을 다루었습니다.

  • Google Benchmark 소개: 정확한 마이크로벤치마킹을 지원하는 라이브러리로, 실행 시간과 메모리 사용량을 측정할 수 있음.
  • 설치 및 설정: Linux 및 Windows에서 Google Benchmark를 설치하고 CMake 또는 Makefile을 사용하여 프로젝트에 적용하는 방법 설명.
  • 기본 벤치마킹 코드 작성: BENCHMARK()BENCHMARK_MAIN()을 사용하여 기본적인 성능 측정 코드 작성.
  • 입력 크기에 따른 성능 분석: RangeMultiplier()를 활용하여 입력 크기에 따른 실행 시간 변화를 분석하는 방법.
  • 반복 실행과 평균 시간 계산: Iterations()MinTime()을 설정하여 보다 정확한 벤치마킹 결과를 얻는 방법.
  • 메모리 사용량 측정: SetBytesProcessed()를 이용해 특정 연산의 메모리 사용량을 분석하는 기법.
  • 최적화 기법 적용 및 성능 비교: 동적 메모리 할당 최적화, 캐시 친화적 접근 방식 적용 후 성능을 비교하는 실험 수행.
  • 실전 예제: 정렬 알고리즘 비교: Quick Sort와 Merge Sort의 벤치마킹을 수행하여 성능 차이를 정량적으로 분석.

Google Benchmark를 활용하면 입력 크기별 성능 변화, 메모리 사용량, 최적화 효과 등을 정밀하게 측정할 수 있으며, 이를 통해 프로그램의 병목을 찾아 개선할 수 있습니다.

벤치마킹을 활용하여 성능을 분석하고, 최적화 전략을 수립하는 것은 고성능 C 애플리케이션 개발에서 필수적인 과정입니다.

목차