プログラムがどのくらいCPUを使ったかを測るには、C言語標準のclock()が手軽です。
CPUが実際に演算していた時間をカウントするため、待機や入出力を含む「壁時計の経過時間」とは意味が異なります。
本記事では、初心者の方でも迷わないようにclock()の基本からサンプル、注意点、環境差まで順を追って詳しく説明します。
C言語のclock()でCPU時間を計測する基本
CPU時間とは?実行時間(壁時計)との違い
CPU時間は、プロセス(プログラム)がCPU上で命令を実行していた時間を表します。
OSによるスケジューリングでCPUを使っていない時間や、ユーザ入力待ち、ディスクやネットワークの入出力待ちなどはほとんど加算されません。
これに対し、壁時計の経過時間(実時間)は、開始から終了までの時計上の時間です。
CPU時間は「計算に費やした純粋な時間」、壁時計時間は「人間が待つ体感時間」と覚えると理解しやすいです。
例えば、巨大なループ計算はCPU時間が大きくなり、ファイルダウンロード待ちやsleep(待機)中はCPU時間はほとんど増えません。
必要なヘッダーと型
clock()は標準ヘッダー<time.h>で宣言されています。
計測値の型はclock_tで、環境によってビット幅が異なる可能性があります。
表示のためにprintfを使うので<stdio.h>もインクルードします。
必須ヘッダーは次の2つです。
#include <time.h>#include <stdio.h>
clock_tの実体は環境依存です。
32bit環境では32bit、Linuxの64bit環境ではしばしば64bit(長整数)である一方、Windowsの64bitでも32bitのことがあります。
CLOCKS_PER_SECで秒に変換する
clock()の戻り値は「クロックティック数」です。
そのティック数を秒へ変換するにはマクロCLOCKS_PER_SECで割ります。
計算時は精度のため浮動小数点にキャストします。
秒への変換式:
seconds = (double)ticks / CLOCKS_PER_SEC;
CLOCKS_PER_SECは環境依存です。
Linux系では1000000のことが多く、Windowsでは1000が一般的です。
値を決め打ちしないで、必ずマクロを使って計算してください。
基本の計測手順
最小限の手順は次の流れです。
必要な処理をstartとendで挟み、差分を秒に直して表示します。
clock_t start = clock();を呼ぶ- 計測したい処理を実行する
clock_t end = clock();を呼ぶclock_t diff = end - start;を取り、(double)diff / CLOCKS_PER_SECで秒へ
clock()が(clock_t)-1を返したら、値が取得できなかったことを意味します。
その場合は測定を中止してエラー扱いにします。
初心者向けサンプルと計測パターン
最小サンプルでCPU時間を計測する
まずは最小の測定プログラムです。
最適化でループが消えないようvolatileを使います。
ソースコード
#include <stdio.h>
#include <time.h>
int main(void) {
// 測定開始時刻(クロックティック)
clock_t start = clock();
if (start == (clock_t)-1) {
// 値が取得できない場合の早期終了
fprintf(stderr, "clock() failed at start\n");
return 1;
}
// 計測対象: 単純な加算ループ(最適化で消されないようにvolatileを使う)
volatile unsigned long long sink = 0; // 結果を外に出すことで最適化抑止
for (unsigned long long i = 0; i < 100000000ULL; ++i) {
sink += i; // 軽いCPU負荷
}
// 測定終了時刻(クロックティック)
clock_t end = clock();
if (end == (clock_t)-1) {
fprintf(stderr, "clock() failed at end\n");
return 1;
}
// 差分(ティック数)と秒への変換
clock_t diff = end - start;
double seconds = (double)diff / CLOCKS_PER_SEC;
printf("CLOCKS_PER_SEC: %ld\n", (long)CLOCKS_PER_SEC);
printf("ticks: %ld\n", (long)diff);
printf("CPU time: %.6f s\n", seconds);
// sink を参照して最適化をさらに抑止
printf("sink: %llu\n", (unsigned long long)sink);
return 0;
}
実際の値は環境に依存しますが、例として次のような出力になります。
CLOCKS_PER_SEC: 1000000
ticks: 230000000
CPU time: 0.230000 s
sink: 4999999950000000
表示のCPU timeは、処理がCPUを使っていた時間です。
処理がI/O待ちやsleep中心なら、実際に待った時間より小さく出ます。
ループ処理のCPU時間を測定する
同じコードでも繰り返し回数を変えるとCPU時間が変化します。
少しだけ負荷を増やして差を見ます。
ソースコード
#include <stdio.h>
#include <time.h>
// 測定ユーティリティ: エラー処理付きclock()ラッパ
static int get_clock(clock_t* out) {
clock_t t = clock();
if (t == (clock_t)-1) return 0;
*out = t;
return 1;
}
int main(void) {
volatile unsigned long long sink = 0;
const unsigned long long N1 = 30000000ULL; // 3千万回
const unsigned long long N2 = 90000000ULL; // 9千万回(3倍の負荷)
// 測定1
clock_t s1, e1;
if (!get_clock(&s1)) return 1;
for (unsigned long long i = 0; i < N1; ++i) sink += i;
if (!get_clock(&e1)) return 1;
// 測定2
clock_t s2, e2;
if (!get_clock(&s2)) return 1;
for (unsigned long long i = 0; i < N2; ++i) sink += i;
if (!get_clock(&e2)) return 1;
double t1 = (double)(e1 - s1) / CLOCKS_PER_SEC;
double t2 = (double)(e2 - s2) / CLOCKS_PER_SEC;
printf("N1=%llu: CPU time = %.6f s\n", (unsigned long long)N1, t1);
printf("N2=%llu: CPU time = %.6f s\n", (unsigned long long)N2, t2);
printf("ratio(N2/N1) ≈ %.2f\n", (t2 > 0.0) ? (t2 / t1) : 0.0);
printf("sink=%llu\n", (unsigned long long)sink);
return 0;
}
回数を3倍にすると、CPU時間もおおむね3倍に近づきます。
わずかな誤差はキャッシュや分解能、最適化などの影響です。
複数区間のCPU時間を累積する
処理A、処理B、処理Cのように区間ごとに測り、その合計CPU時間を得たい場合は、差分ティックを加算していけばよいです。
ソースコード
#include <stdio.h>
#include <time.h>
static void busy_loop(volatile unsigned long long* sink, unsigned long long n) {
for (unsigned long long i = 0; i < n; ++i) {
*sink += i ^ (*sink); // 多少重い演算にする
}
}
int main(void) {
volatile unsigned long long sink = 0;
clock_t total_ticks = 0;
const unsigned long long N[] = { 20000000ULL, 25000000ULL, 30000000ULL };
for (int k = 0; k < 3; ++k) {
clock_t s = clock();
if (s == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
busy_loop(&sink, N[k]);
clock_t e = clock();
if (e == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
clock_t diff = e - s;
total_ticks += diff;
printf("Section %d: ticks=%ld (%.6f s)\n",
k + 1, (long)diff, (double)diff / CLOCKS_PER_SEC);
}
double total_sec = (double)total_ticks / CLOCKS_PER_SEC;
printf("Total CPU time: %.6f s\n", total_sec);
printf("sink=%llu\n", (unsigned long long)sink);
return 0;
}
このように区間計測の差分を合計すれば、全体のCPU時間を柔軟に把握できます。
後述のオーバーフローに注意しつつ、差分単位で扱うのが基本です。
ミリ秒表示に変換する
秒では粗いと感じる場合は、ミリ秒への変換が便利です。
計算式は単純に1000倍です。
ソースコード
#include <stdio.h>
#include <time.h>
static double ticks_to_ms(clock_t ticks) {
// ティック数からミリ秒に変換
return 1000.0 * (double)ticks / CLOCKS_PER_SEC;
}
int main(void) {
volatile unsigned long long sink = 0;
clock_t s = clock();
if (s == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
for (unsigned long long i = 0; i < 40000000ULL; ++i) {
sink += i;
}
clock_t e = clock();
if (e == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
clock_t diff = e - s;
printf("CPU time: %.3f ms\n", ticks_to_ms(diff));
printf("sink=%llu\n", (unsigned long long)sink);
return 0;
}
ミリ秒表示は人間に読みやすい一方、環境によっては1ms未満の差は丸められる可能性があります。
短い処理は繰り返して合計を測るのが効果的です。
簡易ベンチマークの測り方
短時間の処理は1回あたりの計測誤差が大きくなります。
繰り返し回数を自動で増やして、十分な計測時間(例えば50ms以上)を確保してから平均値を出す簡易ベンチマーク例です。
ソースコード
#include <stdio.h>
#include <time.h>
// 計測対象の軽いワークロード(単純な整数演算)
// volatileを介して最適化で消えないようにする
static void workload(unsigned long long n) {
volatile unsigned long long acc = 1;
for (unsigned long long i = 1; i <= n; ++i) {
acc = (acc * 1469598103934665603ULL) ^ i; // 適当な混合
}
// accはわざと使わない(volatileにより外部可視)
}
int main(void) {
const unsigned long long N = 1000000ULL; // 1回分の仕事量
size_t reps = 1;
// 目標の最小計測時間(秒)。短いと分解能の影響が大きい
const double target_seconds = 0.05; // 50ms
double elapsed = 0.0;
clock_t diff = 0;
// 繰り返し回数を自動調整(最大は安全のため制限)
while (reps < 1u << 26) {
clock_t s = clock();
if (s == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
for (size_t r = 0; r < reps; ++r) {
workload(N);
}
clock_t e = clock();
if (e == (clock_t)-1) { fprintf(stderr, "clock() failed\n"); return 1; }
diff = e - s;
elapsed = (double)diff / CLOCKS_PER_SEC;
if (elapsed >= target_seconds) break;
// 達しない場合は回数を倍増
reps *= 2;
}
double per_rep_sec = (reps > 0) ? elapsed / (double)reps : 0.0;
double per_rep_ms = per_rep_sec * 1000.0;
printf("CLOCKS_PER_SEC: %ld\n", (long)CLOCKS_PER_SEC);
printf("reps: %zu, total CPU time: %.3f ms\n", reps, elapsed * 1000.0);
printf("avg per iteration: %.6f ms\n", per_rep_ms);
return 0;
}
十分な計測時間を確保して平均を出すことで、短時間処理でも分解能の影響を緩和できます。
ReleaseビルドとDebugビルドで結果が大きく異なることがあるため、比較は同一ビルド設定で行いましょう。
clock()の注意点とトラブル対策
待機や入出力ではCPU時間は増えにくい
ファイル読み書きやネットワーク待ち、ユーザ入力待ちなどではCPUが遊んでいる時間が長くなります。
CPU時間は「CPUが働いた分」しか増えません。
処理全体の体感時間を測りたい場合は壁時計時間を計測する別の手段が必要です(別記事で扱います)。
短時間の測定は分解能に注意
分解能(最小刻み)より短い処理を1回だけ測ると、結果が0に近づいたりブレが大きくなったりします。
短い処理は複数回繰り返して合計を測るのが基本です。
目安として10ms〜50ms以上の測定時間を確保すると安定します。
clock()が-1を返す場合の確認ポイント
clock()が(clock_t)-1を返すのは、処理系がCPU時間を提供できない場合です。
組込み環境や特殊なライブラリ実装で起こり得ます。
対策としては以下を順に確認します。
- 実行環境のCライブラリが
clock()を実装しているか clock_tのビット幅やCLOCKS_PER_SECの値が正しく取得できるか- 別の計時手段(後述)の検討
CLOCKS_PER_SECは環境依存
絶対に決め打ちしないで、必ずマクロを参照して計算してください。
Linuxでは1000000が多いですが、Windowsでは1000です。
表示時にCLOCKS_PER_SECを一緒に出力しておくとトラブルシュートに役立ちます。
長時間の測定でオーバーフローに注意
オーバーフローは見落としがちな落とし穴です。
clock()は「プロセス開始からのCPU時間の総ティック数」を返すため、clock_tが32bitの場合はいつか上限に達します。
典型例は次の通りです。
- Linux系(多くは
CLOCKS_PER_SEC=1000000)でclock_tが32bitの場合、約2147秒(約35.8分)で飽和 - Windows(MSVC系で
clock_tは32bit、CLOCKS_PER_SEC=1000)では約24.8日で飽和
差分をこまめに取って加算し、極端に長い連続測定を避ける、あるいは「秒に直してdoubleで累積する」などの工夫で回避できます。
マルチスレッドはCPU時間が合算されることが多い
多くの実装でclock()はプロセス全体のCPU時間を返します。
このため複数スレッドが同時にCPUを使えば、CPU時間は「並列に進む」ように増えます。
例えば2スレッドが同時に0.5秒CPUを使えば、プロセスCPU時間は約1秒増えます。
スレッド単位のCPU時間が必要な場合は、別のAPIが必要になります(環境依存、後述)。
環境差とポータビリティ
WindowsとLinuxでのclock()の挙動
代表的な違いを次の表にまとめます。
どちらも基本的に「プロセスのCPU時間」を返しますが、定数や型の実装が異なります。
| 観点 | Linux系(glibcなど) | Windows(MSVCなど) |
|---|---|---|
| 返す時間 | プロセスCPU時間 | プロセスCPU時間 |
| CLOCKS_PER_SEC の典型値 | 1000000 | 1000 |
| clock_t の典型 | 64bit(64bit環境)または32bit(32bit環境) | 多くは32bit |
| -1 の可能性 | まれ | まれ |
| 入出力/待機の扱い | カウントされにくい | カウントされにくい |
| マルチスレッド | 合算されることが多い | 合算されることが多い |
実装差は常にあり得るため、値の決め打ちは避け、実行時に情報を表示しながら開発すると安全です。
32bitと64bitでのclock_tの違い
Linuxの64bit環境(LP64)ではlongが64bitのため、clock_tも64bitであることが多く、オーバーフロー耐性が高いです。
Windowsの64bit(LLP64)ではlongが32bitのままなので、clock_tも32bitの事例が多く、長時間でオーバーフローの可能性があります。
測定が長期に及ぶ可能性があるなら、差分秒をdoubleで累積し、定期的にローテーションする設計を推奨します。
高精度が必要なら別記事で紹介
clock()は手軽で移植性が高い一方、分解能やオーバーフローの制約があります。
より高精度や詳細な測定が必要なら、以下のAPIが候補です(本記事では詳細割愛、別記事で扱います)。
- Linux/Unix:
clock_gettime(CLOCK_PROCESS_CPUTIME_ID)、getrusage、times - Windows:
QueryPerformanceCounter、GetProcessTimes - スレッド単位:
CLOCK_THREAD_CPUTIME_ID(POSIX系)
まずはclock()で大まかな傾向を掴み、必要に応じて高精度APIへ移行するとスムーズです。
まとめ
C言語のclock()は「CPUが実際に働いた時間」を簡単に測れる標準手段です。
使い方は、測定前後でclock()を呼び、差分ティックをCLOCKS_PER_SECで割るだけとシンプルです。
短時間処理では分解能の影響が大きくなるため、繰り返し回数を増やして合計を測る、十分な測定時間(10ms〜50ms以上)を確保する、といった工夫が有効です。
環境によってCLOCKS_PER_SECやclock_tのビット幅が異なるため、決め打ちは避ける、オーバーフローへの配慮をする、(clock_t)-1のエラーチェックをする、という姿勢も重要です。
まずは本記事のサンプルを動かして、CPU時間の感覚を身につけてください。
高精度が必要になった段階で、各OS固有のAPIに進むのが良いステップになります。
