64ビット整数型についてのあれこれです。
ポイント
- FILETIME構造体、ULONGLONG型、ULARGE_INTEGER型の変換は簡単だ。ULONGLONG型を基準に考えるのが良いかもしれない
- __int64型について。ULONGLONG型の正体はunsigned __int64型である
- __int64型のオーバーフローしない加算・乗算について。キャストのルールは16ビット整数や32ビット整数と同じように考えて良いようだ
開発・実行環境
下記の環境でコードを実行し、動作を確かめた。
プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)
compiler: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Visual Studio: 14.0
ワンポイント
テスト用の関数とマクロ
- #define show(EXPRESSION) (_show(#EXPRESSION, (EXPRESSION)))
- void _show(const char* text, unsigned __int64 p) {
- printf("%s\n", text);
- printf(" 0x%I64x\n", p);
- printf("\n");
- }
本題に入る前に、工夫した点について説明する。
このマクロと関数を用いることで、プログラムの計算式と結果を両方同時にコンソールに表示することができる。
符号なし64ビット整数 unsigned __int64 の16進数のフォーマット指定子は%I64xを用いる[1]。
FILETIME、ULONGLONG、ULARGE_INTEGER
CPU時間を取得するGetSystemTimesなどのAPI関数は、測定したCPUのクロックサイクルをFILETIME構造体を通してプログラマに伝える。
FILETIME構造体をそのまま四則演算に用いることはできない。
加算や減算といった演算を行うためには、一度ULONGLONG型もしくはULARGE_INTEGER型に変換するのが便利である。
ULONGLONG
- ULONGLONG a;
- show(a = 0x0123456789abcdef);
ULONGLONG型はunsigned __int64のシノニム[2]である。
これを実行すると以下のようになる。
- a = 0x0123456789abcdef
- 0x123456789abcdef
ULARGE_INTEGER
- ULARGE_INTEGER b;
- show(b.QuadPart = a);
- show(b.LowPart);
- show(b.HighPart);
ULARGE_INTEGER は64ビット整数を含む共用体である[3]。
QuadPartメンバはULONGLONG型であるため、そのまま代入できる。
LowPartメンバとHighPartメンバは、それぞれ上位32ビットと下位32ビットである。
これを実行すると以下のようになる。
- b.QuadPart = a
- 0x123456789abcdef
-
- b.LowPart
- 0x89abcdef
-
- b.HighPart
- 0x1234567
FILETIME
- FILETIME c;
- show(c.dwLowDateTime = (DWORD)(a & 0xffffffff));
- show(c.dwHighDateTime = (DWORD)(a >> 32));
- show(((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime);
FILETIME型はULARGE_INTEGER型の同様、上位32ビットと下位32ビットごとにアクセスできるメンバを持つ[4]。
しかしながら、ULARGE_INTEGER型のQuadPataメンバのように一発で64bit整数を取得できるメンバが存在しない点が異なっている。
そのため、一度ULONGLONG型変数に代入する[5]。
これを実行すると以下のようになる。
- c.dwLowDateTime = (DWORD)(a & 0xffffffff)
- 0x89abcdef
-
- c.dwHighDateTime = (DWORD)(a >> 32)
- 0x1234567
-
- ((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime
- 0x123456789abcdef
このキャスト方法が正解である。
以下のようにやろうとすると、下位32ビット分しか計算できずに失敗する(コンパイラに警告される)。
- show((unsigned)(c.dwHighDateTime << 32) | c.dwLowDateTime);
- show((c.dwHighDateTime << 32) | c.dwLowDateTime);
さて、ULONGLONG型、FILETIME型、ULARGE_INTEGER型についての説明が終わった。
これらの64ビット整数を扱う型はすべてunsigned __int64型がもとになっていることが分かった。
よって、64ビット整数の演算について考えるということは、__int64型の計算について考えることであるといってもよいだろう(良いのか?)。
ここではそういうことにする。
__int64型
__int{8|16|32|64}型はビット長指定のある整数型である[6]。
ポータブルなコードを実現するための型名である。
__int64型のオーバーフローしない加算・乗算
本題である。
キャストの仕方によっては、64ビットで計算されなくなってしまう[7]。
とりあえず、例にならい加算と乗算についてオーバーフローしないキャストの仕方を調べた。
キャストについては奥深いものがある[7]ので、落とし穴を把握しておくことは大切だ。
加算のテストコード
- unsigned int p = 0x10000000;
- unsigned int q = 0xffffffff;
-
- printf("p = 0x%8x\n", p);
- printf("q = 0x%8x\n", q);
- printf("\n");
-
- show(p + q);
- show((unsigned __int64)p + q);
- show((unsigned __int64)(p + q));
- show((unsigned __int64)p + (unsigned __int64)q);
pとqはともに32ビットの符号なし整数である。
キャストなしで加える、加えて括弧なしでキャスト、加えて括弧してキャスト、キャストしてから加える、の四通りのパターンを試す。
加算の実行結果
- p = 0x10000000
- q = 0xffffffff
-
- p + q
- 0xfffffff
-
- (unsigned __int64)p + q
- 0x10fffffff
-
- (unsigned __int64)(p + q)
- 0xfffffff
-
- (unsigned __int64)p + (unsigned __int64)q
- 0x10fffffff
実行結果より、加えて括弧なしでキャスト、キャストしてから加える、のパターンのみオーバーフローしなかった。
乗算のテストコード
- unsigned int r = 0x80000000;
- unsigned int s = 2;
- printf("r = 0x%8x\n", r);
- printf("s = %d\n", s);
- printf("\n");
-
- show(p * r);
- show((unsigned __int64)p * r);
- show((unsigned __int64)(p * r));
- show((unsigned __int64)p * (unsigned __int64)r);
rとsはともに32ビットの符号なし整数である。
キャストなしで掛ける、掛けて括弧なしでキャスト、掛けて括弧してキャスト、キャストしてから掛ける、の四通りのパターンを試す。
乗算の実行結果
- r = 0x80000000
- s = 2
-
- r * s
- 0x0
-
- (unsigned __int64)r * s
- 0x100000000
-
- (unsigned __int64)(r * s)
- 0x0
-
- (unsigned __int64)r * (unsigned __int64)s
- 0x100000000
-
実行結果より、掛けて括弧なしでキャスト、キャストしてから掛ける、のパターンのみオーバーフローしなかった。
まとめ
FILETIME型、ULONGLONG型、ULARGE_INTEGER型などいろいろあるが、元はunsigned __int64型である。
この性質を最も直線的に示す型がULONGLONG型であるため、パフォーマンス測定系のクラスを作成する際には、ULONGLONG型を基底として用いるのがわかりやすいだろう。
あと、64ビット整数のキャストについては、16ビット整数や32ビット整数に当てはまるルールがそのまま当てはまることが分かった。
何か特別なことをしなくても、自然にコーディングすれば事故は起こらないだろう。
感想
今回も脱線してしまった。
CPUパフォーマンス測定はなかなか完了しないなぁ。
付録
今回のコードの最新版は
ここに上げておく。
参考文献
- Format specification syntax: printf and wprintf functions
- ULONGLONG
- ULARGE_INTEGER
- FILETIME
- INFO: Working with the FILETIME Structure
- __int8, __int16, __int32, __int64
- VC++の__int64の罠にハマった\(^o^)/
- 少し詳しい型変換の説明