C言語でプログラムの処理時間を計測するとき、何の関数を使って計測していますか?
もちろん使用する OS 等によって、他の関数を使用することもあると思いますが、おそらく下記の関数を挙げられる方が大半かと思います。
time
関数clock
関数
僕は clock
関数だね!
ミリ秒単位でも計測できるから便利なんだよね
なるほど
でもその理由だけで clock
関数を使うのは危険だよ?
はじめに言っておくと、この time
関数と clock
関数は全く異なる関数です。
ですので、この2つにどのような違いがあるかを理解し、それを理解した上で使い分けをすることが重要です。
このページでは time
関数と clock
関数の違いについて解説し、どのようなプログラムでどのような計測結果の違いが発生するかを実例を踏まえて解説していきたいと思います。
このページは OS が Mac OS X であることを前提に解説しています
他の OS だと挙動が異なる場合があるかもしれません(おそらく Linux は同じ)
この点はご了承ください
Contents
time
関数と clock
関数の違い
まず time
関数と clock
関数の違いについて解説していきます。
time
関数とは
time
関数は実行した時点の「1970年1月1日0時0分からの経過時間」を秒単位で返却する関数になります。
#include <time.h>
time_t time(time_t*);
処理時間を計測したい処理の「開始前」と「終了後」それぞれで time
関数を実行し、その返却値の差分を求めれば、「2つの time
関数実行の間の経過時間を計測」することができます。
#include <stdio.h>
#include <time.h>
int main(void) {
time_t start_time, end_time;
/* 処理開始前の時間を取得 */
start_time = time(NULL);
/* 時間を計測する処理 */
/* 処理終了後の時間を取得 */
end_time = time(NULL);
/* 計測時間の表示 */
printf(
"time:%ld\n",
end_time - start_time
);
return 0;
}
前述の通り time
関数では秒単位でしか時間が計測できないという不便さがあります。
OS 依存になりますが time
関数同様に「経過時間を計測する」関数かつ、秒よりも細かい単位で時間を計測できるものもあります。
例えば Mac や Linux であれば(Windows で使用できるかは未確認)、clock_gettime
関数によりナノ秒単位で経過時間を計測することも可能です。
#include <stdio.h>
#include <sys/time.h>
int main(void) {
unsigned int sec;
int nsec;
double d_sec;
struct timespec start_time, end_time;
/* 処理開始前の時間を取得 */
clock_gettime(CLOCK_REALTIME, &start_time);
/* 時間を計測する処理 */
/* 処理開始後の時間とクロックを取得 */
clock_gettime(CLOCK_REALTIME, &end_time);
/* 処理中の経過時間を計算 */
sec = end_time.tv_sec - start_time.tv_sec;
nsec = end_time.tv_nsec - start_time.tv_nsec;
d_sec = (double)sec
+ (double)nsec / (1000 * 1000 * 1000);
/* 計測時間の表示 */
printf(
"time:%f\n", d_sec
);
return 0;
}
こんな感じで経過時間を計測する関数は他にもありますが、このページでは time
関数に統一して説明していきます。
スポンサーリンク
clock
関数とは
clock
関数は実行した時点までに「プログラムが CPU によって処理された時間(CPU 時間)」を返却する関数になります。
#include <time.h>
clock_t clock(void);
処理時間を計測したい処理の「開始前」と「終了後」それぞれで clock
関数を実行し、その返却値の差分を求めれば、「2つの clock
関数実行の間でプログラムが CPU によって処理された時間を計測」することができます。
#include <stdio.h>
#include <time.h>
int main(void) {
clock_t start_clock, end_clock;
/* 処理開始前のクロックを取得 */
start_clock = clock();
/* 時間を計測する処理 */
/* 処理終了後のクロックを取得 */
end_clock = clock();
/* 計測時間を表示 */
printf(
"clock:%f\n",
(double)(end_clock - start_clock) / CLOCKS_PER_SEC
);
return 0;
}
単位は実行環境によって異なりますが、「CLOCKS_PER_SEC
」で割り算することにより秒単位に変換することができます。小数点未満の数字は秒よりも細かい精度の時間(ms や us など)になります。
time
関数と clock
関数の差
ここまでの解説を読んでくださった方はもうお気づきかもしれませんが、この2つの関数の一番大きな違いは「何を計測するか」です。
time
関数:経過時間を計測clock
関数:CPU 時間を計測
ですので、この2つの違いを理解すれば time
関数と clock
関数の違いを理解することができます。
経過時間
経過時間とはその名の通り(おそらく皆さんが感じた通り)、ある時点からある時点までに経過した時間になります。
処理開始前にストップウォッチをスタートし、処理終了後にストップウォッチをストップして時間を計測する感じです。
CPU 時間
それに対し CPU 時間は「プログラムが CPU によって処理された時間」になります。
処理開始前にストップウォッチをスタートし、処理終了後にストップウォッチをストップして時間を計測する感じなのは経過時間と変わりませんが、途中で CPU がそのプログラムの処理を行なっていない時間は計測されません。
例えば CPU が「他のプログラムの処理を行っている」「何もしていない(遊んでいる)」ような時間は CPU 時間として計測されないです。
なので、本当に CPU が処理している時間のみを計測することができます。
time
関数と clock
関数での計測時間の違い
ではどのような違いが生じるのか、具体的にプログラムと一緒に確認していきたいと思います。
スポンサーリンク
違いがほぼ生じない場合
例えば下記のようなプログラムでほぼ演算ばかりを行なっている場合、time
関数と clock
関数との計測時間にほぼ差は生じません。
#include <stdio.h>
#include <time.h>
#define N 100000
int main(void) {
int i, j;
int sum;
time_t start_time, end_time;
clock_t start_clock, end_clock;
/* 処理開始前の時間とクロックを取得 */
start_time = time(NULL);
start_clock = clock();
/* ループ処理 */
sum = 0;
for (j = 0; j < N; j++) {
for (i = 0; i < N; i++) {
sum += j * N + i;
}
}
/* 処理開始後の時間とクロックを取得 */
end_time = time(NULL);
end_clock = clock();
/* 計測時間の表示 */
printf(
"time:%ld\n",
end_time - start_time
);
printf(
"clock:%f\n",
(double)(end_clock - start_clock) / CLOCKS_PER_SEC
);
return 0;
}
実際に計測時間を表示すると、私の PC では下記のように表示され、time
関数と clock
関数との計測時間にほぼ差が出ませんでした。
time:35 clock:34.781402
time
関数と clock
関数とで差が発生しないのは、時間を計測したプログラムがほぼ CPU しか利用しないためです。
常に CPU が稼働してプログラムが処理されるので、ほぼ両者の関数での計測時間に差が出ていません。
sleep
処理が含まれる場合
次は time
関数と clock
関数との計測時間に差が出る分かりやすい例です。
#include <stdio.h>
#include <time.h>
#include <unistd.h>
int main(void) {
time_t start_time, end_time;
clock_t start_clock, end_clock;
/* 処理開始前の時間とクロックを取得 */
start_time = time(NULL);
start_clock = clock();
/* スリープ処理 */
sleep(5);
/* 処理開始後の時間とクロックを取得 */
end_time = time(NULL);
end_clock = clock();
/* 計測時間の表示 */
printf(
"time:%ld\n",
end_time - start_time
);
printf(
"clock:%f\n",
(double)(end_clock - start_clock) / CLOCKS_PER_SEC
);
return 0;
}
私の PC だと計測結果は下記のようになりました。
time:5 clock:0.000053
sleep
関数は引数で指定した秒数が経過するまでプログラムを停止させる関数です。
この停止している間 CPU はそのプログラムに対する処理を行いません(他のプログラムの処理を行う・休む)。
ですので sleep
関数で停止させた分、time
関数と clock
関数での計測時間に差が発生することになります。
他のハードの処理で待たされる場合
次も time
関数と clock
関数との計測時間に差が出る例です。
#include <stdio.h>
#include <time.h>
#define N (1024 * 1024 * 1024)
char data[N];
int main(void) {
FILE *fp;
time_t start_time, end_time;
clock_t start_clock, end_clock;
/* ファイルオープン */
fp = fopen("test.bin", "wb");
/* 処理開始前の時間とクロックを取得 */
start_time = time(NULL);
start_clock = clock();
/* ファイル書き込み */
fwrite(data, N, 1, fp);
/* 処理開始後の時間とクロックを取得 */
end_time = time(NULL);
end_clock = clock();
/* 計測時間の表示 */
printf(
"time:%ld\n",
end_time - start_time
);
printf(
"clock:%f\n",
(double)(end_clock - start_clock) / CLOCKS_PER_SEC
);
/* ファイルをクローズ */
fclose(fp);
return 0;
}
計測時間は下記のように差が発生しています。
time:8 clock:5.971490
コンピュータではいろんなハードウェア(メモリや HDD など)が連携して動作して様々な処理を実現します。
基本的に CPU は他のハードウェアに比べて処理が早いので、他のハードウェアの処理が終わるのを待たされて、結果的に CPU 時間が短くなることもあります。
上の例ではディスクに書き込みの完了を CPU が待っているため、CPU がアイドルになる時間(もしくは他のプログラムを処理している時間)が発生しているため time
関数と clock
関数との計測時間に差が発生しています。
スポンサーリンク
マルチスレッド処理の場合
ここまでは time
関数での計測時間の方が clock
関数での計測時間よりも長くなる例でしたが、今回は clock
関数の方が長くなる例です。
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#define N 50000
#define NUM_THREAD 4
typedef struct {
int start;
int end;
int sum;
} data_t;
void *thread_func(void *arg) {
int i, j;
int start, end;
int sum = 0;
start = ((data_t*)arg)->start;
end = ((data_t*)arg)->end;
/* ループ処理 */
for (j = start; j < end; j++) {
for (i = 0; i < N; i++) {
sum += j * N + i;
}
}
((data_t*)arg)->sum = sum;
return NULL;
}
int main(void) {
int i;
int sum;
pthread_t threads[NUM_THREAD];
data_t data[NUM_THREAD];
time_t start_time, end_time;
clock_t start_clock, end_clock;
/* 処理開始前の時間とクロックを取得 */
start_time = time(NULL);
start_clock = clock();
sum = 0;
/* スレッド作成 */
for (i = 0; i < NUM_THREAD; i++) {
data[i].start = N / NUM_THREAD * i;
data[i].end = N / NUM_THREAD * (i + 1);
data[i].sum = 0;
pthread_create(
&threads[i],
NULL,
thread_func,
&data[i]
);
}
/* スレッド終了 */
for (i = 0; i < NUM_THREAD; i++) {
pthread_join(
threads[i],
NULL
);
}
/* 合計の計算 */
for (i = 0; i < NUM_THREAD; i++) {
sum += data[i].sum;
}
/* 処理開始後の時間とクロックを取得 */
end_time = time(NULL);
end_clock = clock();
/* 計測時間の表示 */
printf(
"time:%ld\n",
end_time - start_time
);
printf(
"clock:%f\n",
(double)(end_clock - start_clock) / CLOCKS_PER_SEC
);
return 0;
}
計測結果は下記のようになりました。
time:3 clock:12.895398
マルチスレッドとは1つのプログラムの処理を分割することで、複数の CPU コアを同時に利用して処理を高速化するプログラミング手法です。
上記のプログラムは、違いがほぼ生じない場合で示した例のループ部分を4つの領域に分割し、4つのコアで並列に処理を行うようにした例になります。
このように並列に処理を行う場合、CPU コアは複数分同時に処理を行うので、「プログラムが CPU によって処理された時間」が並列度分重なって計測されることになり、その分経過時間に対して CPU 時間が長くなります。
基本的には、ループを分割したとしても、ループで処理する演算の総量は同じなので、マルチスレッドにしようがしまいが CPU 時間に変わりはありません
ただしスレッドを生成したりスレッドの切り替えなどが発生するため、シングルスレッドのプログラムに比べてマルチスレッドの方が CPU 時間は若干長くなります
また処理が並列して行われるため経過時間は並列度分短くなることになります(理想的にはシングルスレッドに比べて経過時間は「1 / 並列度」になる)
が、こちらもマルチスレッドにすることによるオーバーヘッドのせいで、理想的な処理時間に比べて長くなります
他のプログラムの処理で待たされる場合
今度は2つのプログラムを同時に動かした時の time
関数と clock
関数の計測時間の差についてみていきたいと思います。
用意するプログラムの1つは下記になります。このプログラムは16個のスレッドで無限ループをひたすら回すものになります。
#include <stdio.h>
#include <pthread.h>
#define NUM_THREAD 16
void thread_func(void *arg) {
while (1) {
}
}
int main(void) {
int i;
pthread_t thread[NUM_THREAD];
for (i = 0; i < NUM_THREAD; i++) {
pthread_create(&thread[i], NULL, thread_func, NULL);
}
for ( i = 0; i < NUM_THREAD; i++) {
pthread_join(thread[i], NULL);
}
}
用意する2つ目のプログラムは違いがほぼ生じない場合で示したプログラムです(ここでは省略します)。
1つ目のプログラム実行後、この2つ目のプログラムを実行してみます。
1つ目のプログラムは無限ループで回っているので終わりませんが、2つ目のプログラムは待っていると処理が終了します。終わった時に表示された結果は下記のようになりました。
time:172 clock:38.856780
違いがほぼ生じない場合の結果と比べると、time
関数と clock
関数の差が大きくなっていることが確認できると思います。
OS やスレッド生成時のオプション等にもよるのですが、CPU のコアは処理すべきスレッドがたくさんある場合、あるスレッドの処理を一定時間単位だけ処理を行い、その時間が経過すると他のスレッドの処理を行うようにスケジューリングして処理が実行されます。
つまり、上記の2つ目のプログラムは、1つ目のプログラムのスレッドが CPU で実行されている間は、自分の処理の順番が回ってくるのを待つことになります。
この待ちの間は CPU 時間としては計測されませんが、経過時間としては計測されるので、上記のように time
関数と clock
関数とでの計測結果に大きな違いが発生することになります。
また clock
関数での計測結果は、上記の結果のように、他のプログラムの影響を受けにくいということも確認できると思います。
time
関数と clock
関数との使い分け
ここまでの説明で time
関数と clock
関数の違いは理解していただけたのではないかと思います。
違いは分かった!
だけどどちらを使うのが正解なの?
前述の通り、この2つでは「経過時間」を計測するか、「CPU 時間」を計測するかが異なります。
では処理時間を計測する際、どちらの関数を使うべきでしょうか?
基本的には time
関数になると思います。time
関数というか、経過時間で計測するのがよいと思います。
なぜなら人が直感的に感じる時間を表しているのは CPU 時間ではなく経過時間だからです。
もし秒単位よりも細かい精度で計測したいのであれば time
関数とはで紹介した clock_gettime
や gettimeofday
関数などを使用すればよいです。
ただし、下記のような場合は CPU 時間の計測でもよいと思います。
- ほぼ CPU のみが処理を行う場合
- 他のプログラムの影響を受けずに処理時間を計測したい場合
- 単純に CPU が処理する処理量のみを比較する場合
例えば2つのアルゴリズムの演算量を純粋に比較するのであれば clock
関数が有効だと思います。メモリアクセスによる待ち時間等を無視して比較することができるはずです。
また経過時間と CPU 時間を両方計測することで、そのプログラムで CPU がどれだけ遊んでいるか(暇をしているか)を確認することも可能です。
経過時間に比較して CPU 時間が極端に短い場合、それだけ CPU が遊んでいることになります。ですので、マルチスレッド処理などで他の処理を並行して実行することで、遊んでいる CPU を有効利用し、システム全体のパフォーマンスを向上させることも可能です。
気をつけないと…
処理時間の計測といっても、その計測の目的は様々だと思います。その目的に応じ、経過時間 or CPU 時間の計測、もしくはこの2つの併用を使い分けることが重要です。
スポンサーリンク
まとめ
このページでは time
関数と clock
関数の違いについて解説しました。
この2つは冒頭でも触れたように全く異なる関数になります。
この2つの関数の違いをしっかり理解し、目的に応じて使い分けるようにしましょう!
何気なく使っていたので勉強になりました!ありがとうございます。
Kちゃん
コメントありがとうございます!
そういっていただけるとありがたいです。
知っておくと役に立つ知識だと思いますので、今後活用していただけると幸いです。