/ «2003-09-30 (Tue) ^ 2003-10-02 (Thu)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2003-10-01 (Wed)

[Time][Writing] RDTSC 命令 --凄いぞ、64ビットカウンター!--

さて、DJ Bernstein 氏が着目した RDTSC 命令であるが、この暗号のような名前は ReaD Time Stamp Counter に由来している。

余り知られていないが、Pentium 以降の x86 シリーズは、クロックレベルの解像度を持った64ビットカウンターを内部に有している(MSR: Model Specific Registers のひとつ)。この超高精度カウンターは、電源ONでゼロリセットされ、CPU 稼働中は1クロックサイクル毎に1ずつインクリメントされる。RDTSC 命令は本カウンターの現在値を EDX/EAX レジスターペアに格納するための命令である。

これだけではあまり実感が湧かないので、実際に計算してみよう。1GHz のクロック周波数をもった CPU の1クロックサイクルは、1/10^9 秒、すなわち1ナノ秒。光の速度をもってしても、30cm しか進めないほどの短い間隔で、64ビットカウンターは回り続けることになる。

頭の中でイメージすると、あっと言う間にカウンターがオーバーフローしてしまいそうな感じがするが、64ビットのパワーは底知れないものがある。bc を使って計算してみよう。まずは、64ビット長で表現可能な最大の整数値(unsigned)はどれぐらいの大きさになるのだろうか?

$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'. 
2^64-1
18446744073709551615

なんと20桁である。見ただけでゲンナリする程だが、この数値に1GHzの場合の1クロック時間を掛け合わせてみる。

(2^64-1)/10^9
18446744073

かなり、小さくなった。1GHz CPU の64ビットカウンターが一杯になるのは、電源が投入されてから184億秒後である。まだ、実感が湧かない。

(2^64-1)/10^9/(60*60*24)
213503
(2^64-1)/10^9/(60*60*24)/365
584

そうですか、584年後ですか。後10回生まれ変わったとしても、心配いらない訳ね。失礼しました。

long long

本題に戻る。これも余り知られていないのだが、x86 GCC は64ビット長整数値に対応しており、long long 宣言を使用することができる。int は32ビットだが、long long を使うことで、一気に64ビットの世界を堪能することが可能になるのだ。

これに併せて、当然のことながら glibc も対応済みである。取り急ぎ、私達が知りたいのは printf での出力制御である。次の ll.c を見てほしい。

#include <stdio.h>     // printf()

int main() {
  int i = -1;
  long long l = -1;

  printf("%X?n", i);
  printf("%llX?n", l);

  printf("%u?n", i);
  printf("%llu?n", l);
 }

このプログラムを見るだけで、GCC 上での64ビット整数値の使いこなしのポイントはお分かり頂けるだろう。それでは、実行してみよう。

$ gcc -o ll ll.c
$ ./ll
FFFFFFFF
FFFFFFFFFFFFFFFF
4294967295
18446744073709551615

詳細は省くが、int, long long 変数共に ー1を指定しているのは、このバイナリー値が unsigned では最大値に相当するからである。実際、実行結果は先ほど計算した 2^32-1, 2^64-1 と同じ値になっている。

なお、printf 中で64ビットを表示するためには、このプログラム例のように "ll" を前置する必要があるので注意してほしい。"ll" を指定しなければ、32ビット分の情報しか表示されない。詳細は、man 3 printf 中の "The length modifier" を参照してほしい。

戻り値の正体

それでは、RDTSC 命令・・と行きたいところだが、その前にどうしても、理解しておかねばならないことがある。それは、関数の戻り値の正体である。

これも世の中の教科書にはほとんど書かれていないのだが、実践では極めて重要。x86 GCC における次の事実は、知っておいて損はない。

  • int の戻り値は EAX レジスターを通じて呼び出し元に伝えられる
  • long long の戻り値は EDX/EAX レジスターペアーを通じて呼び出し元に伝えられる

しかし、これも所詮は机上の知識に過ぎない。実際に、次に示す simple_func.c で確認してみよう。

int simple_func(void);                 // Declare simple_func as an
                                              // function entry address

asm("simple_func:             ");      // simple_func entry address
asm("  movl  $123, %eax    ");      // return 123
asm("  ret                        ");

int main() {
  return simple_func();
 }

このコードは GCC のインラインアセンブリ機能を用いて、simple_func 関数をまるごとアセンブリ言語で記述したものである。この3行のアセンブリソースは、C言語で simple_func() { return 123; } で記述した場合と同じ(正確にはより短縮した)コードに相当する。狐につままれた人も多いかと思うが、ともかく実行してみよう。

$ gcc -o simple_func simple_func.c
$ ./simple_func ; echo $?
123

意図通り、親プロセスであるシェルに123が返されていることから、アセンブリ言語版 simple_func 関数は正常に動作していることが分かる。

機械語を知らずしてC言語を語るなかれ

simple_func.c は、アセンブリ言語を知らない人が見れば、思わず面食らってしまう内容だが、実は機械語こそがC言語の正体なのである。逆に言えば、アセンブリ言語・機械語を知らずして、C言語を制覇することは到底不可能だと、私は信じている。

にもかかわらず、世の中のC言語入門書は、一冊としてアセンブリ言語理解の必要性を説くものはない。それどころか、「1週間で分かる」、「すぐ分かる」、「猫でも分かる」などと甘い言葉で読者を誘い、「皆さん分かりますか?変数というのは箱なんですよ」という決まり文句で、善良なる初心者を迷宮に招き入れるのである。「変数=箱」という概念に囚われている限り、C言語を体得することは不可能。「変数=関数=構造体=メモリー」の境地に達した時、初めてC言語の本質が見えてくるのだが、このためには機械語の知識が必須である。

志を胸に秘めてC言語入門書を手に取ったものの、途中で挫折した若い人達の数は一体どれだけの数に上ることか?しかし、挫折した人はまだいい。中には「自分はC言語を理解できた」と思いこんでいる読者も少なくない。実は、これが一番たちが悪い。そして、「思いこませる」技術に長けた著者のテキストが、ベストセラーになるという悪循環が、この日本で続いている。

RDTSC をこの目で見る

日本が置かれた暗い影ばかり見ていても仕方がない。世界には、ワクワクするようなことが満ちあふれている。知的好奇心を満たしてくれるものが、毎日のように登場している。人生、楽しまなければ損である。しかし、心底楽しむためには、それなりの技術と知識・経験が必要。プログラミングも、機械語の知識が加わることで、スクリプト言語やコンパイラーとは全く違う楽しみ方が出来るようになる。やってみよう。

先ほど紹介したように、Pentium CPU 内部のタイマーカウンターは64ビット長である。これに対してレジスターは32ビット長しかない。困った、半分のサイズしかないよ・・。

でも大丈夫。x86 では64ビット長の場合は、EDX および EAX レジスターの2つを組み合わせることで、演算を遂行することができるのである。本当?紙に書かれた文言ではなく、この目で確認することが大切だ。

#include <stdio.h>                          // printf()

long long simple_longlong(void);      // Declare simple_longlong as an
                                                        // function entry address

asm("simple_longlong:           ");       // simple_longlong entry address
asm("  movl  $0x55667788, %eax  "); // return 0x1122334455667788
asm("  movl  $0x11223344, %edx  ");
asm("  ret                      ");

int main() {
  printf("%llx?n", simple_longlong());
  return 0;
 }

64ビットでデータを扱う場合、上位32ビット(ワード)は EDX レジスター、下位32ビットは EAX レジスターに格納する決まりになっている。このため、simple_longlong 関数では、EDX に 0x11223344、EAX に 0x55667788 を転送した。

$ gcc -o longlong longlong.c 
$ ./longlong 
1122334455667788

ブラボ〜!おっしゃる通りだね。それでは、準備は全て完了した。いよいよ RDTSC 命令を実行してみよう。このためには、インラインアセンブリーを用いて、Cソース中に直接 rdtsc 命令を展開する必要がある。

#include <stdio.h>                     // printf()

long long exec_rdtsc(void);         // Declare exec_rdtsc as an
                                                  // function entry address

asm("exec_rdtsc:             ");        // exec_rdtsc entry address
asm("  rdtsc                   ");          // Do RDTSC instruction
asm("  ret                      ");

int main() {
  printf("%lld?n", exec_rdtsc());
  return 0;
 }

rdtsc.c は上のようになった。簡単である。asm() 文中に rdtsc と記載しているだけである。後は、ret (RETurn)命令を使って、呼び出し元へ帰るのみ。おみやげに、64ビットカウンターの値を EDX/EAX レジスターに携えながら。それでは、気合いを入れて実行。

$ gcc -o rdtsc rdtsc.c
$ ./rdtsc 
3225565264110002

お〜〜、いかにもそれらしい値が現れた。この値を検証してみよう。まず最初に、マシンの CPU およびそのクロック周波数を確認しておく。これには、/proc/ ディレクトリの cpuinfo が便利だ。

$ cat /proc/cpuinfo 
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model            : 11
model name    : Intel(R) Celeron(TM) CPU                1000MHz
stepping         : 1
cpu MHz         : 1002.306
cache size       : 256 KB
fdiv_bug         : no
hlt_bug           : no
f00f_bug         : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 2
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 mmx fxsr sse
bogomips        : 1998.84

Celeron 1GHz であることが分かった。uptime からのデータを元に、rdtsc が返した値を検算してみよう。

$ uptime ; ./rdtsc
 18:51:06  up 37 days,  6:23,  3 users,  load average: 1.02, 1.01, 1.00
3227200233459745
$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'. 
3227200233459745/(60*60*24)
37351854553

64ビットカウンターから逆算すると、このサーバーの uptime は 37.35 日であることが分かる。uptime の表示値とは多少ズレがあるが、これこそが今後検討すべき課題である。

「時間道」は、まだ始まったばかりなのだ。