벤치마킹과 프로파일링: 정확한 코드 성능 측정
객관적인 데이터 없이 코드를 최적화하면 실수가 발생합니다. 캐시 지역성을 개선하기 위해 다시 작성된 해시 함수가 실행 속도를 15% 늦추었습니다. 직관은 속이기 쉽습니다—시간과 프로세서 이벤트의 정확한 측정이 필요합니다.
벤치마킹 프레임워크가 이 문제를 해결합니다: 여러 실행, 통계 분석, perf와의 통합. 이를 통해 실제 병목 현상을 식별할 수 있습니다.
고정밀 타이밍 방법
표준 time()은 1초 해상도만 제공합니다—마이크로 최적화에는 받아들일 수 없습니다.
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:
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 인터럽트, 분기 때문에 단일 실행은 무의미합니다.
기본 통계
#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 performancetaskset -c 0nice -n -20
중요한 사항
- CPU 카운터: rdtsc/rdcycle—정확성의 금준
- 통계: 중앙값 + stddev가 평균보다 신뢰성 높음
- perf 이벤트: cache-misses가 지역성 문제 드러냄
- 캐시 워밍업: 현실적 측정에 필수
- Volatile: 데드 코드 최적화 방지
— Editorial Team
아직 댓글이 없습니다.