【C言語】時間計測に用いる time 関数と clock 関数の違いは?

time関数とclock関数の違い解説ページのアイキャッチ

C言語でプログラムの処理時間を計測するとき、何の関数を使って計測していますか?

もちろん使用する OS 等によって、他の関数を使用することもあると思いますが、おそらく下記の関数を挙げられる方が大半かと思います。

  • time 関数
  • clock 関数

僕は clock 関数だね!

ミリ秒単位でも計測できるから便利なんだよね

なるほど

でもその理由だけで clock 関数を使うのは危険だよ?

え???

はじめに言っておくと、この time 関数と clock 関数は全く異なる関数です。

ですので、この2つにどのような違いがあるかを理解し、それを理解した上で使い分けをすることが重要です。

このページでは time 関数と clock 関数の違いについて解説し、どのようなプログラムでどのような計測結果の違いが発生するかを実例を踏まえて解説していきたいと思います。

MEMO

このページは OS が Mac OS X であることを前提に解説しています

他の OS だと挙動が異なる場合があるかもしれません(おそらく Linux は同じ)

この点はご了承ください

time 関数と clock 関数の違い

まず time 関数と clock 関数の違いについて解説していきます。

time 関数とは

time 関数は実行した時点の「1970年1月1日0時0分からの経過時間」を秒単位で返却する関数になります。

time関数
#include <time.h>
time_t time(time_t*);

処理時間を計測したい処理の「開始前」と「終了後」それぞれで time 関数を実行し、その返却値の差分を求めれば、「2つの time 関数実行の間の経過時間を計測」することができます。

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 時間)」を返却する関数になります。

clock関数
#include <time.h>
clock_t clock(void);

処理時間を計測したい処理の「開始前」と「終了後」それぞれで clock 関数を実行し、その返却値の差分を求めれば、「2つの clock 関数実行の間でプログラムが CPU によって処理された時間を計測」することができます。

clock関数での処理時間計測
#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 関数の違いを理解することができます。

経過時間

経過時間とはその名の通り(おそらく皆さんが感じた通り)、ある時点からある時点までに経過した時間になります。

処理開始前にストップウォッチをスタートし、処理終了後にストップウォッチをストップして時間を計測する感じです。

time関数での処理時間計測のイメージ

CPU 時間

それに対し CPU 時間は「プログラムが CPU によって処理された時間」になります。

処理開始前にストップウォッチをスタートし、処理終了後にストップウォッチをストップして時間を計測する感じなのは経過時間と変わりませんが、途中で CPU がそのプログラムの処理を行なっていない時間は計測されません

例えば CPU が「他のプログラムの処理を行っている」「何もしていない(遊んでいる)」ような時間は CPU 時間として計測されないです。

clock関数での処理時間のイメージ

なので、本当に 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 が稼働してプログラムが処理されるので、ほぼ両者の関数での計測時間に差が出ていません。

常にCPUが処理している時の時間計測

sleep 処理が含まれる場合

次は time 関数と clock 関数との計測時間に差が出る分かりやすい例です。

sleep処理が含まれる場合
#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 関数での計測時間に差が発生することになります。

sleepを行った時の処理時間計測

他のハードの処理で待たされる場合

次も 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 関数との計測時間に差が発生しています。

HDD書き込みを行う際の処理時間計測

スポンサーリンク

マルチスレッド処理の場合

ここまでは 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 時間が長くなります。

マルチスレッド 処理時の処理時間計測

MEMO

基本的にループを分割してもループで処理を演算量は同じなのでマルチスレッドにしようがしまいが CPU 時間に変わりはありません

ただしスレッドを生成したりスレッドの切り替えなどが発生するため、シングルスレッドのプログラムに比べてマルチスレッドの方が CPU 時間は若干長くなります

また処理が並列して行われるため経過時間は並列度分短くなることになります(理想的にはシングルスレッドに比べて経過時間は「1 / 並列度」になる)

が、こちらもマルチスレッドにすることによるオーバーヘッドのせいで、理想的な処理時間に比べて長くなります

他のプログラムの処理で待たされる場合

今度は2つのプログラムを同時に動かした時の time 関数と clock 関数の計測時間の差についてみていきたいと思います。

用意するプログラムの1つは下記になります。このプログラムは16個のスレッドで無限ループをひたすら回すものになります。

CPUを使用し続けるプログラム
#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_gettimegettimeofday 関数などを使用すればよいです。

ただし、下記のような場合は CPU 時間の計測でもよいと思います。

  • ほぼ CPU のみが処理を行う場合
  • 他のプログラムの影響を受けずに処理時間を計測したい場合
  • 単純に CPU が処理する処理量のみを比較する場合

例えば2つのアルゴリズムの演算量を純粋に比較するのであれば clock 関数が有効だと思います。メモリアクセスによる待ち時間等を無視して比較することができるはずです。

また経過時間と CPU 時間を両方計測することで、そのプログラムで CPU がどれだけ遊んでいるか(暇をしているか)を確認することも可能です。

経過時間に比較して CPU 時間が極端に短い場合、それだけ CPU が遊んでいることになります。ですので、マルチスレッド処理などで他の処理を並行して実行することで、遊んでいる CPU を有効利用し、システム全体のパフォーマンスを向上させることも可能です。

サボってるのがバレるんだね…

気をつけないと…

(サボってたのか…)

処理時間の計測といっても、その計測の目的は様々だと思います。その目的に応じ、経過時間 or CPU 時間の計測もしくはこの2つの併用を使い分けることが重要です。

スポンサーリンク

まとめ

このページでは time 関数と clock 関数の違いについて解説しました。

この2つは冒頭でも触れたように全く異なる関数になります。

この2つの関数の違いをしっかり理解し、目的に応じて使い分けるようにしましょう!

2 COMMENTS

daeu

Kちゃん

コメントありがとうございます!

そういっていただけるとありがたいです。
知っておくと役に立つ知識だと思いますので、今後活用していただけると幸いです。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です