時間情報の取得方法と扱い方

作成:

ここではC言語での時間情報の取得方法について説明していく、 時間情報には様々な種類があり、また環境により利用できる関数も変わってくる。 C言語の環境であればどこでも利用できる標準関数から始まり、POSIX環境、Windows環境で利用できるAPIも紹介する。

C言語標準

引き続きC言語の標準関数で、C言語が使える環境であればどこでも使用できる方法を紹介する。

clock()

time()関数に続いて紹介するのは、clock()関数である。 初めに断っておくが、この関数は、「時刻」を扱う関数ではないし、 「実時間」を扱う関数でもない。「 CPU 時間」を扱う関数である。

プロトタイプ宣言は以下。 ここで出てくる、clock_tも、time_t同様、 現在の多くのシステムでは符号付き 64bit 整数で扱われる。

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

戻り値は、そのプロセスの「 CPU 時間」である。 そのプロセスの実行にどれだけの時間 CPU が使用されたか、を示す。

仮に、シングルスレッドのプログラムで 100% CPU を専有するような場合は、ほぼ経過した実時間を示す。 逆に CPU を使用しない状況、スリープで待たせている場合や、 コンテキストスイッチで他のプロセスが実行されている場合は、 どれだけ実時間が経過しても、 CPU の実行時間としては消費されないため増加しない。 また、マルチプロセッサ(マルチコア)環境で複数のスレッドを実行している場合は、 各 CPU コアの時間の合計となるため、経過した実時間より大きな時間となることもある。

この値の単位であるが、これは定義値CLOCKS_PER_SECで決められている。 名前の通り、この値で割れば秒単位の値が得られる。 多くのシステムでは 1000000 で定義されている場合が多い。 Cygwin などの Windows 上のシステムの場合 1000 になっているようだ。 いずれにせよ、この値はあくまで単位を表しており、精度とは異なる点に注意。 1000000 で定義されているからといって、マイクロ秒単位の精度が得られるという意味ではない。

また、clock()を1回だけコールし得られた値については、 どこからカウントした値かは決められていない。 そのため、意味のある値を得るには開始と終了でそれぞれ値をとり、差を利用する必要がある。

以下のコードを実行してみる。 やっていることは、clock()time()を使って、 ビジーループとスリープを実行している間の時間を計測している。

一番最初のループはtime()の値が変化するまで待つために入れている。

#include <stdio.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char **argv) {
  clock_t c_start, c_end;
  time_t t_start, t_end;
  t_start = time(NULL);
  while(time(NULL) - t_start == 0);
  c_start = clock();
  t_start = time(NULL);
  while(time(NULL) - t_start < 2);
  c_end = clock();
  t_end = time(NULL);
  printf("%f\n", (double)(c_end - c_start) / CLOCKS_PER_SEC);
  printf("%ld\n", t_end - t_start);
  c_start = clock();
  t_start = time(NULL);
  sleep(2);
  c_end = clock();
  t_end = time(NULL);
  printf("%f\n", (double)(c_end - c_start) / CLOCKS_PER_SEC);
  printf("%ld\n", t_end - t_start);
  return 0;
}

実行結果は以下のようになる

1.999142
2
0.000043
2

前半は、シングルスレッドのビジーループであるため、実時間に近い値が出ている。 一方、後半はスリープを行い、CPU 時間を消費していないため、実時間に比較してごく小さな値になっている。

さて、この関数で計測できるのは実時間ではないので、プログラム内で実時間の経過を取得する目的では利用できない。 しかし、時刻情報ではないためtime()のように時刻設定による影響を受けない。 また、プロセスが CPU を利用した時間が得られるということは、 計測している間に動作している他のプロセス等が消費した時間が含まれない、 プロセス単体の実行時間が得られるという意味である。 プログラムの実行負荷や CPU の性能比較という用途に向いた値が得られるといえるだろう。

現在はclock_tが 64bit なので特に心配する必要はないが、かつては 32bit 整数で表現されていた。 その場合、CLOCKS_PER_SECが、1000の場合、24日20時間31分23秒ちょっとで、 1000000の場合は35分47秒ちょっとで桁あふれが発生するため、扱いに注意が必要だった。

これに関連して、実時間でもミリ秒単位の時間を 32bit 変数で扱うシステムは多く、 起動してから約24~25日(符号あり)とか約49~50日(符号なし)で問題が発生する。 などというバグ報告を見かけたら、ミリ秒時間を 32bit で扱っていて、 桁あふれへの対処にミスが有った可能性が高い。

カウンター値は想定される現実的な使用範囲では桁あふれしないようなデータ形式を利用する。 桁あふれを回避できない場合は、溢れても問題なく動作する工夫が必要だ。 その考慮漏れがよく起こるのが時間の扱いなので紹介した。