第2章 情報の表現と操作
2章について
この章では、コンピュータ上における数値の扱い方について詳しく書かれていました。
IEEE浮動小数点や、整数型の演算、算術シフトにおける注意点など、幅広く扱っています。
以下まとめです。
データ・サイズ
ワード・サイズがあり、仮想アドレスはこのワードによってエンコードされる。近年ワードサイズは、32から64ビット移行が進んでいる。
典型的なサイズやコンパイラの設定に依存するのを避けるため、固定のデータ・サイズ持つ型もある。
- int32_t : 厳密に4バイト
- int64_t : 厳密に8バイト
ちなみにポインタはプログラムの1ワード・サイズを使う。
エンディアンとは?
2バイト以上で表現される数値の、メモリへの格納方式のこと。
リトル・エンディアン : 下から上にバイトを格納する。
ビッグ・エンディアン : 上から下にバイトを格納する。
バイ・エンディアン : リトル・エンディアン、ビッグ・エンディアンを切り替えられる。
エンディアンが違うマシンでのやりとりにおける注意点
- バイトの順序が逆になる。
- キャストした場合に異なる結果が出る。
論理処理とシフト演算
論理演算
- OR(||), AND(&&), NOT(!)
- 引数が0以外ならTRUE、0ならFALSEを表している。
- 結果がTRUEなら1、FALSEであれば0を返す。
式 結果 !0x42 -> 0x00 !0x00 -> 0x01 !!0x42 -> 0x01 0x42 && 0x24 -> 0x01 0x42 || 0x24 -> 0x01
シフト演算
コンピュータは2進数を扱っているため、2の累乗の乗算と除算は、ビットをずらすことで計算できる。
[01100011] x << 4 [00110000] x >> 4 [00000110] (論理シフト) x >> 4 [11111110] (算術シフト)
注意点は、負数の右シフトで、算術シフトが行われるかどうかは処理系定義であること。
そのため、左シフトを使って演算するなどの工夫があると良さそう。
#include <stdio.h> int main() { int x = -1024; int y = x >> 1; // 算術シフトが行われるかは処理系定義 int z = x / (1<<1); // 左シフトを使うことで安全に演算できる printf("y = %d, z = %d\n", y, z); return 0; }
符号付きと符号なしキャスト
Cでは異なる数値データ型をキャストする事ができる。
あくまでビットの解釈を変えるだけで、ビット値自体は同じ。
#include <stdio.h> int main() { int i = -1; unsigned int ui = (unsigned int)i; int j = (int)ui; // i = -1, ui = 4294967295, j = -1 printf("i = %d, ui = %u, j = %d\n", i, ui, j); return 0; }
そもそも全部符号付き整数型にしておけば良さそう?と思ったが、符号なしが便利になる場合もあるらしい。
例としては、数字をビットの集まりとして考えたい場合やアドレスを扱う場合などがある。
加算
符号なし加算
x + y が 2w - 1 より大きい場合、和はオーバーフローする。(w = ワードサイズ)
4ビットで表現される場合(2^4-1=15) x = 9 [1001] y = 12[1100] x + y = 21[10101]([1111]=15よりも大きい) 上位ビットを破棄して5[0101]になる。 これは 21 mod 16 = 5 と一致する。
二の補数の加算
ワードサイズが4の場合(-8 ≤ x < 8) の範囲が扱える
4ビットで表現される場合 x = -8[1000] y = -5[1011] x + y = -13[10011] 上位ビットを破棄して3[0011]になる。 x = 5[0101] y = 5[0101] x + y = 10[01010] 上位ビットを破棄して-6[1010]になる。
これにより、INT_MAX([1111...] 31ビット全て立っている状態)に 1 を加算すると
INT_MIN([1000...] 32ビット目(符号ビット)だけが立っている状態) になるということが分かりますね。
乗算と除算
定数の乗算 - 定数の乗算をシフトと加算の組合わせに置き換えることで命令数が減らせる。
乗算の置き換え 11[1011]<<2 = 44[101100] (11*4 = 44 と同じ) 14 = 2^4 - 2^1 x * 14 = (x<<4) - (x<<1) # 置き換えることで最適化できる。
二の累乗による除算
除算は乗算よりも遅いので、同じくシフト演算を使うことで最適化を図る。
符号なし数 k >> k (二進数) (12340/2^k) 0 [0011000000110100] = 12340 12340.0 1 [0001100000011010] = 6170 6170.0 4 [0000001100000011] = 771 771.25 8 [0000000000110000] = 48 48.203125
浮動小数点
非常に大きい数や0に非常に近い数を伴う計算を行ったり、実数の算術を近似できる。
現在は、IEEE浮動小数点が一般的に扱われている。
各桁の重み付けは小数点記号('.')との相対位置で定まる。
- は と表される 。
- 2進数 は と表される。
IEEE浮動小数点表現
x と y を用いて の形式の数で表す。
IEEEの標準規格は数を、 の形式で表す。
- 符号 s
は、正負を表す(正なら 0
、負なら 1
)。
- 仮数 M
は、 0 以上で基数より小さい数の範囲。
- 指数 E
は、基数の何乗かを表す。
- 基数は、n進数のnに当たる部分(2または10)。
浮動小数点数のビット表現をこれらの値をエンコードするために三つの部分に分ける。
(例)
123.456 78
を、仮数部が整数の十進浮動小数点数で表すと、仮数は 12345678
、指数は −5
となる。
式で表すと
正規化
expが全て0または 1(単精度で255、倍精度で2047)のどちらでもない場合。
ケチ表現
二進法で正規化をすると、最上位ビットは常に 1 になるので、これを表さず常に 1 があるものとみなす省略が可能。
この省略した表現をケチ表現と言う。
省略することで、仮数部に割り当てたビット数が n
であれば、有効桁数は n+1
となる。
非正規化
- 指数フィールドが全て
0
の場合、表される数は非正規化値である。 - 指数部が である。
- 数値0、また0.0に非常に近い数を表す。
特殊な値
また特殊な値として、浮動小数点には特殊な値として、inf や NaN も表現できる。
- 小数フィールドが全て0の場合、 なら の、 なら を表す。
- 少数フィールドが非ゼロの場合、
NaN
を表す。
丸め
表現の範囲と精度が限られているため、実数の算術を近似するにすぎない。
そのため、4種類の丸め演算を使い、値に対して表せる最も近い値を見つける。
- 偶数丸め、0への丸め、切り下げ、切り上げの4種類
ちなみにデフォルトでは偶数丸めが使われている(最も近い整数の値となる)
Cにおける浮動小数点
C言語ではfloat(単精度)
と double(倍精度)
がある。
偶数丸めが使われており、丸めモードを変更したり、 のような特殊な値を得る標準的な方法はない。
また、キャストする際には、丸めにより望んだ結果が得られない場合があるため注意する。
浮動小数点演算
算術演算の結果として丸めを適用した値が返される。
IEEEの強みは、演算結果がハードウェアやソフトウェアに依存しないという点にある。
重要な特性として、浮動小数点演算は結合的ではない( (a+b)+c=a+(b+c)
が成り立たない場合があるということ)
例)
となりが丸みによって消える。
となる。
このように浮動小数点数の演算においては、順番を気をつける必要がある。
感想
そもそもコンピュータが情報をビットで表現しているということから始まり、浮動小数点についてまで順を追って学ぶことができました。
正直難しいなーと思うこともありましたが、図や表で視覚的に説明されてあり、なんとか読み進める事ができました。
また、これまではビットの扱いが苦手でしたが、今後は必要に応じて使っていこうと思います。
なんとなく知っているけど、詳細はよく知らないな。というものが、あーそういうことだったのね。という体験がたくさんできるので、興味ある方はぜひ読んでみてください。