홈으로 돌아가기

코드 벤치마킹: 정밀한 CPU 및 캐시 측정

고정밀 CPU 사이클 측정 및 perf counters로 벤치마킹 프레임워크 만드는 챕터. x86, RISC-V, ARM 예제. 통계 분석 및 일반 오류.

정밀 벤치마크: rdtsc부터 perf 분석까지
Advertisement 728x90

벤치마킹과 프로파일링: 정확한 코드 성능 측정

객관적인 데이터 없이 코드를 최적화하면 실수가 발생합니다. 캐시 지역성을 개선하기 위해 다시 작성된 해시 함수가 실행 속도를 15% 늦추었습니다. 직관은 속이기 쉽습니다—시간과 프로세서 이벤트의 정확한 측정이 필요합니다.

벤치마킹 프레임워크가 이 문제를 해결합니다: 여러 실행, 통계 분석, perf와의 통합. 이를 통해 실제 병목 현상을 식별할 수 있습니다.

고정밀 타이밍 방법

표준 time()은 1초 해상도만 제공합니다—마이크로 최적화에는 받아들일 수 없습니다.

Google AdInline article slot

clock_gettime()

struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
run_test();
clock_gettime(CLOCK_MONOTONIC, &end);

long ns = (end.tv_sec - start.tv_sec) * 1000000000L +
          (end.tv_nsec - start.tv_nsec);

장점: 나노초 해상도, 시스템 시간 조정에 강함, POSIX 이식성.

CPU 사이클 카운터 (권장)

RISC-V:

static inline uint64_t rdcycle(void) {
    uint64_t cycles;
    asm volatile ("rdcycle %0" : "=r" (cycles));
    return cycles;
}

x86_64:

Google AdInline article slot
static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    asm volatile ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

ARM64:

static inline uint64_t rdcycle(void) {
    uint64_t val;
    asm volatile("mrs %0, pmccntr_el0" : "=r"(val));
    return val;
}

해상도—1 사이클, 시스템 콜 오버헤드 없음. 단점: 아키텍처 종속적, 주파수 변화 영향 받음.

결과의 통계 분석

캐시 변동, OS 인터럽트, 분기 때문에 단일 실행은 무의미합니다.

Google AdInline article slot

기본 통계

#define ITERATIONS 1000
uint64_t times[ITERATIONS];
// ... fill times ...

uint64_t min = times[0], max = times[0], sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
    if (times[i] < min) min = times[i];
    if (times[i] > max) max = times[i];
    sum += times[i];
}
uint64_t mean = sum / ITERATIONS;

고급 통계

중앙값은 이상치에 강하고, 표준편차는 안정성을 나타냅니다:

qsort(times, ITERATIONS, sizeof(uint64_t), compare_uint64);
uint64_t median = times[ITERATIONS / 2];

double variance = 0;
for (int i = 0; i < ITERATIONS; i++) {
    double diff = (double)times[i] - (double)mean;
    variance += diff * diff;
}
double stddev = sqrt(variance / ITERATIONS);

주요 지표:

  • Minimum: 최적 경우 (warm cache)
  • Median: 전형적인 성능
  • Stddev: 변동성
  • Maximum: 최악 경우

벤치마킹 프레임워크

재사용을 위한 범용 인터페이스:

typedef struct {
    const char *name;
    void (*setup)(void);
    void (*run)(void);
    void (*teardown)(void);
} benchmark_t;

void benchmark_run(benchmark_t *bench, int iterations);

구현 내용:

  • 워밍업 실행
  • 여러 측정
  • 통계 보고서

예시:

int array[1000];

void setup_array(void) {
    for (int i = 0; i < 1000; i++) {
        array[i] = i;
    }
}

void test_sequential_access(void) {
    volatile int sum = 0;
    for (int i = 0; i < 1000; i++) {
        sum += array[i];
    }
}

benchmark_t bench = {
    .name = "Sequential Array Access",
    .setup = setup_array,
    .run = test_sequential_access,
    .teardown = NULL
};

benchmark_run(&bench, 1000);

perf를 사용한 캐시 분석

perf stat이 하드웨어 카운터를 추적합니다:

$ perf stat -e cache-references,cache-misses ./program
  1,234,567 cache-references
     12,345 cache-misses # 1.00% miss rate

유용한 이벤트:

  • cache-references/misses: 모든 레벨
  • L1-dcache-loads/misses: L1 데이터
  • LLC-loads/misses: 마지막 레벨

구조 비교: array—1.2K misses, linked list—45K misses (37배 나쁨).

프레임워크에 perf 통합

typedef struct {
    uint64_t cycles;
    uint64_t cache_references;
    uint64_t cache_misses;
    uint64_t l1_loads;
    uint64_t l1_misses;
} perf_counters_t;

perf_event_open 카운터의 자동 수집 및 집계.

일반적인 문제와 해결책

컴파일러 최적화

// Bad: 루프 제거됨
int sum = 0;
for (...) sum += array[i];

// Good
volatile int sum = 0;

Cold vs Warm Cache

첫 실행이 느립니다. 해결: 워밍업 + 별도 지표.

측정 오버헤드

rdcycle()은 ~10 사이클 소요. 빼기 또는 긴 테스트 사용.

시스템 노이즈

  • 많은 반복
  • 평균 대신 중앙값
  • cpupower frequency-set -g performance
  • taskset -c 0
  • nice -n -20

중요한 사항

  • CPU 카운터: rdtsc/rdcycle—정확성의 금준
  • 통계: 중앙값 + stddev가 평균보다 신뢰성 높음
  • perf 이벤트: cache-misses가 지역성 문제 드러냄
  • 캐시 워밍업: 현실적 측정에 필수
  • Volatile: 데드 코드 최적화 방지

— Editorial Team

Advertisement 728x90

다음 읽기