[2026] C++ 스톱워치와 벤치마크 | chrono으로 실행 시간 측정하기
이 글의 핵심
C++ 스톱워치와 벤치마크: chrono으로 실행 시간 측정하기. C++에서 시간 측정·같이 보면 좋은 글 (내부 링크).
C++에서 시간 측정
실행 시간을 재려면 std::chrono의 시계와 duration을 쓰면 됩니다. high_resolution_clock이 보통 가장 짧은 단위(나노초 수준)를 제공하므로, 짧은 구간을 잴 때 적합합니다. 측정한 duration을 초·밀리초로 바꿀 때는 시간 변환을 참고하면 됩니다. 실무에서는 함수 한 번 호출 시간, 루프 N회 평균, 로그 구간 타이밍 등에 자주 씁니다.
간단한 스톱워치
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <iostream>
class Stopwatch {
using Clock = std::chrono::high_resolution_clock;
Clock::time_point start_;
public:
Stopwatch() : start_(Clock::now()) {}
void reset() { start_ = Clock::now(); }
double elapsed_ms() const {
auto end = Clock::now();
return std::chrono::duration<double, std::milli>(end - start_).count();
}
};
int main() {
Stopwatch sw;
volatile int x = 0;
for (int i = 0; i < 1000000; ++i) x += i;
std::cout << "elapsed: " << sw.elapsed_ms() << " ms\n";
return 0;
}
실무 팁: steady_clock을 쓰면 시스템 시계 보정의 영향을 받지 않아, 경과 시간만 측정할 때 더 적합할 수 있습니다. high_resolution_clock은 구현에 따라 steady_clock의 별칭일 수도 있고 아닐 수도 있으므로, “monotonic이 꼭 필요하다”면 steady_clock을 명시하는 것이 좋습니다.
RAII 스타일 구간 측정
생성자에서 시작, 소멸자에서 끝을 재서 구간 전체 시간을 잡는 패턴입니다. 예외가 나도 소멸자가 호출되므로 안전합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class ScopedTimer {
using Clock = std::chrono::steady_clock;
Clock::time_point start_;
const char* name_;
public:
explicit ScopedTimer(const char* name = nullptr) : start_(Clock::now()), name_(name) {}
~ScopedTimer() {
auto ms = std::chrono::duration<double, std::milli>(Clock::now() - start_).count();
if (name_) std::cout << "[" << name_ << "] ";
std::cout << ms << " ms\n";
}
};
void process() {
ScopedTimer t("process");
// ....작업 ...
} // 소멸 시 자동으로 경과 시간 출력
벤치마크: 여러 번 돌리고 해석
한 번만 재면 캐시 상태·스케줄링에 따라 편차가 큽니다. 여러 번 돌린 뒤 평균·중앙값·백분위를 보는 것이 좋습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <vector>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <cmath>
template<typename F>
struct BenchmarkResult {
double min, max, mean, median, stddev;
std::vector<double> samples;
};
template<typename F>
BenchmarkResult<F> benchmark(F&& f, int runs = 100) {
std::vector<double> times;
times.reserve(runs);
// 워밍업 (캐시, JIT 등)
for (int i = 0; i < 3; ++i) f();
// 실제 측정
for (int i = 0; i < runs; ++i) {
auto start = std::chrono::steady_clock::now();
f();
auto end = std::chrono::steady_clock::now();
times.push_back(std::chrono::duration<double, std::milli>(end - start).count());
}
std::sort(times.begin(), times.end());
BenchmarkResult<F> result;
result.samples = times;
result.min = times.front();
result.max = times.back();
result.median = times[runs / 2];
result.mean = std::accumulate(times.begin(), times.end(), 0.0) / runs;
// 표준편차 계산
double variance = 0.0;
for (double t : times) {
variance += (t - result.mean) * (t - result.mean);
}
result.stddev = std::sqrt(variance / runs);
return result;
}
int main() {
volatile int sink = 0;
auto result = benchmark([&sink]() {
for (int i = 0; i < 1000000; ++i) sink += i;
}, 100);
std::cout << "Min: " << result.min << " ms\n";
std::cout << "Max: " << result.max << " ms\n";
std::cout << "Mean: " << result.mean << " ms\n";
std::cout << "Median: " << result.median << " ms\n";
std::cout << "StdDev: " << result.stddev << " ms\n";
return 0;
}
통계 해석:
- Min: 최상의 경우 (캐시 히트, CPU 할당 최적)
- Max: 최악의 경우 (캐시 미스, 컨텍스트 스위칭)
- Mean: 평균 (이상치에 민감)
- Median: 중앙값 (이상치에 덜 민감, 일반적 성능)
- StdDev: 표준편차 (변동성, 낮을수록 안정적) 실무 권장:
- 중앙값(Median) 을 주로 보고, 표준편차가 크면 측정 횟수를 늘리거나 환경을 안정화하세요.
- 워밍업: 처음 몇 번은 캐시가 차가워서 느릴 수 있으므로, 워밍업 후 측정합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 백분위 계산 (P95, P99)
double percentile(const std::vector<double>& sorted_times, double p) {
int idx = static_cast<int>(sorted_times.size() * p);
return sorted_times[std::min(idx, (int)sorted_times.size() - 1)];
}
auto result = benchmark(f, 100);
std::cout << "P95: " << percentile(result.samples, 0.95) << " ms\n";
std::cout << "P99: " << percentile(result.samples, 0.99) << " ms\n";
최적화 제거 방지
컴파일러가 “결과를 쓰지 않는다”고 판단하면 루프나 호출 자체를 제거할 수 있습니다. 벤치마크할 코드가 실제로 실행되도록 하려면:
- 결과를 사용:
volatile에 쓰거나, 결과를 반환해 외부에서 사용하게 만듦. - 컴파일러 장벽:
asm volatile("")로 최적화 방지. - 도구 사용: Google Benchmark, nanobench 등은 반복 횟수 조절·통계 출력을 해 주고, 최적화 제거를 줄이는 패턴을 적용해 둠. 다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 나쁜 예: 최적화 시 제거될 수 있음
void bad_bench() {
int x = 0;
for (int i = 0; i < 1000000; ++i) x += i;
// x를 사용하지 않으므로 루프 전체가 제거될 수 있음
}
// ✅ 나은 예 1: 결과를 volatile에 저장
volatile int sink = 0;
void better_bench() {
int x = 0;
for (int i = 0; i < 1000000; ++i) x += i;
sink = x; // volatile 쓰기는 side effect
}
// ✅ 나은 예 2: 컴파일러 장벽 사용
void best_bench() {
int x = 0;
for (int i = 0; i < 1000000; ++i) x += i;
asm volatile("" : "+r"(x) : :); // x를 사용했다고 컴파일러에 알림
}
실무 예시 - Google Benchmark 스타일: 다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// DoNotOptimize: 값이 최적화되지 않도록 보장
template<typename T>
void DoNotOptimize(T const& value) {
asm volatile("" : : "r,m"(value) : "memory");
}
// ClobberMemory: 메모리 상태를 변경했다고 컴파일러에 알림
void ClobberMemory() {
asm volatile("" : : : "memory");
}
// 사용
void benchmark_function() {
int result = expensive_computation();
DoNotOptimize(result); // 결과가 사용되었다고 표시
}
주의사항: volatile은 성능 오버헤드가 있으므로, 측정 대상 코드 내부가 아닌 결과를 저장할 때만 사용하세요.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 루프 내부에서 volatile 사용 (느림)
volatile int x = 0;
for (int i = 0; i < 1000000; ++i) {
x += i; // 매번 메모리 쓰기
}
// ✅ 루프 외부에서 volatile 사용
int x = 0;
for (int i = 0; i < 1000000; ++i) {
x += i; // 레지스터에서 계산
}
volatile int sink = x; // 한 번만 메모리 쓰기
벤치마크 모범 사례
1. 충분한 반복 횟수
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 너무 적은 반복
auto result = benchmark(f, 5); // 편차가 클 수 있음
// ✅ 충분한 반복
auto result = benchmark(f, 100); // 통계적으로 의미 있음
권장 반복 횟수:
- 빠른 함수 (< 1ms): 1000회 이상
- 보통 함수 (1-100ms): 100회
- 느린 함수 (> 100ms): 10-30회
2. 워밍업
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 워밍업으로 캐시 안정화
for (int i = 0; i < 10; ++i) {
f(); // 캐시 워밍업
}
// 실제 측정
for (int i = 0; i < 100; ++i) {
auto start = steady_clock::now();
f();
auto elapsed = steady_clock::now() - start;
// ....기록 ...
}
3. 이상치 제거
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 이상치 제거 (상위/하위 10% 제거)
std::vector<double> times = /* ....*/;
std::sort(times.begin(), times.end());
int trim = times.size() / 10;
std::vector<double> trimmed(times.begin() + trim, times.end() - trim);
double mean = std::accumulate(trimmed.begin(), trimmed.end(), 0.0) / trimmed.size();
4. 환경 안정화
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// CPU 주파수 고정 (Linux)
// sudo cpupower frequency-set --governor performance
// 프로세스 우선순위 높이기
// nice -n -20 ./benchmark
// CPU 코어 고정
#include <pthread.h>
void pin_to_core(int core_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core_id, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
}
자주 발생하는 문제
1. 시계 선택 오류
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ system_clock으로 경과 시간 측정
auto start = system_clock::now();
// ....시스템 시간 변경 가능 ...
auto elapsed = system_clock::now() - start; // 음수 가능!
// ✅ steady_clock 사용
auto start = steady_clock::now();
auto elapsed = steady_clock::now() - start; // 항상 양수
권장: wall-clock이 필요하면 system_clock, 경과 시간만 필요하면 steady_clock을 쓰세요. high_resolution_clock은 해상도는 높지만 steady가 보장되지 않을 수 있습니다.
2. 한 번만 측정
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 한 번만 측정
auto start = steady_clock::now();
f();
auto elapsed = steady_clock::now() - start;
std::cout << "Time: " << elapsed.count() << "\n"; // 편차 큼
// ✅ 여러 번 측정 후 통계
auto result = benchmark(f, 100);
std::cout << "Median: " << result.median << " ms\n";
std::cout << "StdDev: " << result.stddev << " ms\n";
왜 중요한가?: 첫 실행은 캐시가 차가워서 느리고, 이후 실행은 캐시 히트로 빠를 수 있습니다. 여러 번 측정해 중앙값을 보는 것이 안정적입니다.
3. 최적화 제거
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 결과를 사용하지 않아 최적화로 제거
int compute() {
int sum = 0;
for (int i = 0; i < 1000000; ++i) sum += i;
return sum;
}
void bench() {
compute(); // 반환값을 사용하지 않으면 제거될 수 있음
}
// ✅ 결과를 사용
volatile int sink;
void bench() {
sink = compute(); // 결과 사용
}
실무 패턴
패턴 1: 함수별 성능 프로파일링
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 여러 함수의 성능을 비교
struct ProfileResult {
std::string name;
double median_ms;
};
std::vector<ProfileResult> profile_functions() {
std::vector<ProfileResult> results;
results.push_back({"Algorithm A", benchmark(algorithm_a, 100).median});
results.push_back({"Algorithm B", benchmark(algorithm_b, 100).median});
results.push_back({"Algorithm C", benchmark(algorithm_c, 100).median});
std::sort(results.begin(), results.end(),
{ return a.median_ms < b.median_ms; });
for (const auto& r : results) {
std::cout << r.name << ": " << r.median_ms << " ms\n";
}
return results;
}
패턴 2: 입력 크기별 성능 측정
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 알고리즘의 시간 복잡도 확인
void measure_complexity() {
std::vector<int> sizes = {100, 1000, 10000, 100000};
for (int n : sizes) {
auto result = benchmark([n]() {
std::vector<int> v(n);
std::sort(v.begin(), v.end());
}, 50);
std::cout << "n=" << n << ": " << result.median << " ms\n";
}
}
패턴 3: 비교 벤치마크
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 두 구현의 성능 비교
void compare_implementations() {
auto result_old = benchmark(old_implementation, 100);
auto result_new = benchmark(new_implementation, 100);
double speedup = result_old.median / result_new.median;
std::cout << "Old: " << result_old.median << " ms\n";
std::cout << "New: " << result_new.median << " ms\n";
std::cout << "Speedup: " << speedup << "x\n";
if (speedup > 1.0) {
std::cout << "New is " << (speedup - 1.0) * 100 << "% faster\n";
} else {
std::cout << "New is " << (1.0 - speedup) * 100 << "% slower\n";
}
}
정리
| 항목 | 설명 |
|---|---|
| 도구 | steady_clock / high_resolution_clock, duration |
| 패턴 | Stopwatch 클래스, RAII ScopedTimer, N회 측정 후 통계 |
| 통계 | 평균, 중앙값, 표준편차, 백분위 |
| 주의 | 최적화 제거 방지, 워밍업, 환경 안정화 |
FAQ
Q: 몇 번 측정해야 하나요?
A: 함수 실행 시간에 따라 다릅니다. 빠른 함수 (< 1ms)는 1000회 이상, 보통 함수 (1-100ms)는 100회, 느린 함수 (> 100ms)는 10-30회 측정하세요.
Q: 평균과 중앙값 중 어느 것을 봐야 하나요?
A: 중앙값(Median)을 주로 보세요. 평균은 이상치에 민감하지만, 중앙값은 이상치에 덜 민감하여 일반적인 성능을 더 잘 나타냅니다.
Q: 표준편차가 크면 어떻게 하나요?
A: 측정 환경이 불안정하다는 의미입니다. 측정 횟수를 늘리거나, 다른 프로세스를 종료하거나, CPU 주파수를 고정하세요.
Q: 최적화 제거를 어떻게 방지하나요?
A: 결과를 volatile 변수에 저장하거나, asm volatile로 컴파일러 장벽을 두거나, Google Benchmark 같은 전문 라이브러리를 사용하세요.
Q: steady_clock과 high_resolution_clock 중 어느 것을 사용하나요?
A: steady_clock을 권장합니다. 단조 증가가 보장되어 시스템 시간 변경에 영향받지 않습니다. high_resolution_clock은 구현에 따라 steady_clock의 별칭일 수 있습니다.
관련 글: duration 시간 간격, 시간 변환 duration_cast, time_point, chrono 개요.
한 줄 요약: chrono으로 스톱워치를 만들고, 벤치마크는 여러 번 측정·통계 해석하며 최적화 제거를 고려하면 됩니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.