2019年10月13日日曜日

64ビット整数型について

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

ワンポイント

テスト用の関数とマクロ


  1. #define show(EXPRESSION) (_show(#EXPRESSION, (EXPRESSION)))
  2. void _show(const char* text, unsigned __int64 p) {
  3. printf("%s\n", text);
  4. printf(" 0x%I64x\n", p);
  5. printf("\n");
  6. }

本題に入る前に、工夫した点について説明する。
このマクロと関数を用いることで、プログラムの計算式と結果を両方同時にコンソールに表示することができる。
符号なし64ビット整数 unsigned __int64 の16進数のフォーマット指定子は%I64xを用いる[1]。

FILETIME、ULONGLONG、ULARGE_INTEGER

CPU時間を取得するGetSystemTimesなどのAPI関数は、測定したCPUのクロックサイクルをFILETIME構造体を通してプログラマに伝える。
FILETIME構造体をそのまま四則演算に用いることはできない。
加算や減算といった演算を行うためには、一度ULONGLONG型もしくはULARGE_INTEGER型に変換するのが便利である。

ULONGLONG

  1. ULONGLONG a;
  2. show(a = 0x0123456789abcdef);

ULONGLONG型はunsigned __int64のシノニム[2]である。
これを実行すると以下のようになる。

  1. a = 0x0123456789abcdef
  2. 0x123456789abcdef

ULARGE_INTEGER 

  1. ULARGE_INTEGER b;
  2. show(b.QuadPart = a);
  3. show(b.LowPart);
  4. show(b.HighPart);

ULARGE_INTEGER は64ビット整数を含む共用体である[3]。
QuadPartメンバはULONGLONG型であるため、そのまま代入できる。
LowPartメンバとHighPartメンバは、それぞれ上位32ビットと下位32ビットである。
これを実行すると以下のようになる。

  1. b.QuadPart = a
  2. 0x123456789abcdef
  3.  
  4. b.LowPart
  5. 0x89abcdef
  6.  
  7. b.HighPart
  8. 0x1234567

FILETIME

  1. FILETIME c;
  2. show(c.dwLowDateTime = (DWORD)(a & 0xffffffff));
  3. show(c.dwHighDateTime = (DWORD)(a >> 32));
  4. show(((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime);

FILETIME型はULARGE_INTEGER型の同様、上位32ビットと下位32ビットごとにアクセスできるメンバを持つ[4]。
しかしながら、ULARGE_INTEGER型のQuadPataメンバのように一発で64bit整数を取得できるメンバが存在しない点が異なっている。
そのため、一度ULONGLONG型変数に代入する[5]。
これを実行すると以下のようになる。

  1. c.dwLowDateTime = (DWORD)(a & 0xffffffff)
  2. 0x89abcdef
  3.  
  4. c.dwHighDateTime = (DWORD)(a >> 32)
  5. 0x1234567
  6.  
  7. ((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime
  8. 0x123456789abcdef

このキャスト方法が正解である。
以下のようにやろうとすると、下位32ビット分しか計算できずに失敗する(コンパイラに警告される)。

  1. show((unsigned)(c.dwHighDateTime << 32) | c.dwLowDateTime);
  2. 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]ので、落とし穴を把握しておくことは大切だ。

加算のテストコード

  1. unsigned int p = 0x10000000;
  2. unsigned int q = 0xffffffff;
  3.  
  4. printf("p = 0x%8x\n", p);
  5. printf("q = 0x%8x\n", q);
  6. printf("\n");
  7.  
  8. show(p + q);
  9. show((unsigned __int64)p + q);
  10. show((unsigned __int64)(p + q));
  11. show((unsigned __int64)p + (unsigned __int64)q);

pとqはともに32ビットの符号なし整数である。
キャストなしで加える、加えて括弧なしでキャスト、加えて括弧してキャスト、キャストしてから加える、の四通りのパターンを試す。

加算の実行結果

  1. p = 0x10000000
  2. q = 0xffffffff
  3.  
  4. p + q
  5. 0xfffffff
  6.  
  7. (unsigned __int64)p + q
  8. 0x10fffffff
  9.  
  10. (unsigned __int64)(p + q)
  11. 0xfffffff
  12.  
  13. (unsigned __int64)p + (unsigned __int64)q
  14. 0x10fffffff

実行結果より、加えて括弧なしでキャスト、キャストしてから加える、のパターンのみオーバーフローしなかった。

乗算のテストコード

  1. unsigned int r = 0x80000000;
  2. unsigned int s = 2;
  3. printf("r = 0x%8x\n", r);
  4. printf("s = %d\n", s);
  5. printf("\n");
  6.  
  7. show(p * r);
  8. show((unsigned __int64)p * r);
  9. show((unsigned __int64)(p * r));
  10. show((unsigned __int64)p * (unsigned __int64)r);

rとsはともに32ビットの符号なし整数である。
キャストなしで掛ける、掛けて括弧なしでキャスト、掛けて括弧してキャスト、キャストしてから掛ける、の四通りのパターンを試す。

乗算の実行結果

  1. r = 0x80000000
  2. s = 2
  3.  
  4. r * s
  5. 0x0
  6.  
  7. (unsigned __int64)r * s
  8. 0x100000000
  9.  
  10. (unsigned __int64)(r * s)
  11. 0x0
  12.  
  13. (unsigned __int64)r * (unsigned __int64)s
  14. 0x100000000
  15.  

実行結果より、掛けて括弧なしでキャスト、キャストしてから掛ける、のパターンのみオーバーフローしなかった。

まとめ

FILETIME型、ULONGLONG型、ULARGE_INTEGER型などいろいろあるが、元はunsigned __int64型である。
この性質を最も直線的に示す型がULONGLONG型であるため、パフォーマンス測定系のクラスを作成する際には、ULONGLONG型を基底として用いるのがわかりやすいだろう。

あと、64ビット整数のキャストについては、16ビット整数や32ビット整数に当てはまるルールがそのまま当てはまることが分かった。
何か特別なことをしなくても、自然にコーディングすれば事故は起こらないだろう。

感想

今回も脱線してしまった。
CPUパフォーマンス測定はなかなか完了しないなぁ。

付録

今回のコードの最新版はここに上げておく。

参考文献

  1. Format specification syntax: printf and wprintf functions
  2. ULONGLONG
  3. ULARGE_INTEGER 
  4. FILETIME 
  5. INFO: Working with the FILETIME Structure
  6. __int8, __int16, __int32, __int64
  7. VC++の__int64の罠にハマった\(^o^)/
  8. 少し詳しい型変換の説明

0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています