ざっくりZig - ビット幅と演算

2024/03/14に公開

コンピュータは扱える数値の範囲が決まっています。執筆時現在では64ビットCPUを持つPCが多いですが、この「64ビット」がそれです。これを ビット幅 といいます。実はビット幅が決まっていることではじめて実現できる計算の方法があります。Zigではそれがうまく考慮されているのが特徴の1つです。

ビット幅と型

インテルはかつて4ビットのCPUを開発していました[1]。そこから8ビット、16ビット、32ビット、そして64ビットというようにどんどんビット幅が広がっていきました。それに伴い、記憶容量が増えるなどしてコンピュータはどんどん高性能化していきました。

Zigではこのビット幅を として表現します。型というのは、データの扱い方を規定するもので、整数型、実数型のほかに定義済みの型がいくつかあります。

整数型

整数型は符号なし、符号ありに分かれています。符号とは、-1などの負の数を表す-(マイナス)のことで、符号ありの場合は最上位(左端)のビットが1のとき負の数を表します。

ビット幅はともに0~65535までとなっています。ただ、0ビットの整数型はソースコードには書けても記憶領域は確保されないため、実質的には何もできません。また、リファレンスには8, 16, 32, 64, 128だけが記されていて、それ以外はほとんど言及されていません。

ビット幅 符号なし 符号あり
8 u8 i8
16 u16 i16
32 u32 i32
64 u64 i64
128 u128 i128

実数型

小数点がつく実数型のビット幅は16, 32, 64, 80, 128のものがあります[2]。実数型は123.0e-45のような形式で扱われ、eより左を 仮数部 、右を 指数部 といい、それぞれビット幅が決められています。このほか整数型と同様に符号を表すビットが1つあり、3つの合計が全体のビット幅になります。

ビット幅 符号ビット 仮数部ビット数(mantissa) 指数部ビット数(exponent)
16 f16 1 10 5
32 f32 1 23 8
64 f64 1 52, 11
80 f80 1 64 15
128 f128 1 112 15

各ビット幅で扱える数値の範囲とバイト数

ビット幅は、コンピュータが扱える数値の2進数での桁数であるともいえます。8ビットであれば、2進数の8桁で表せる数値を扱えるということです。では、各ビット幅の具体的な範囲を確認してみます。

  • 符号なし整数型
最小値 最大値 バイト数
u8 0 255 1
u16 0 65535 2
u32 0 4294967295 4
u64 0 18446744073709551615 8
u128 0 340282366920938463463374607431768211455 16
  • 符号あり整数型
最小値 最大値 バイト数
i8 -128 127 1
i16 -32768 32767 2
i32 -2147483648 2147483647 4
i64 -9223372036854775808 9223372036854775807 8
i128 -170141183460469231731687303715884105728 170141183460469231731687303715884105727 16
  • 実数型(e[+-]nは10のn乗を示す) - 符号は除く
最小値(正規化) 最大値(正規化) バイト数
f16 6.104e-5 6.55e4 2
f32 1.1754944e-38 3.4028235e38 4
f64 2.2250738585072014e-308 1.7976931348623157e308 8
f80 3.3621031431120935063e-4932 1.189731495357231765e4932 16
f128 3.3621031431120935062626778173217526e-4932 1.189731495357231765085759326628007e4932 16

型を宣言して代入

変数に値を代入するときに型を宣言できます。型名は変数名の次に : 型名 を続けます。これにより、その変数で扱える値の範囲が決まります。

const a: u8 = 255;  // aには0から255までの値を代入できる

オーバーフローと演算

計算の結果が型で扱える範囲を超えることを オーバーフロー (Overflow)といいます。u8の場合は0から255までの数しか扱えませんので、255 + 1の結果は(正の)オーバーフローを起こすことになります。

このとき、Zigでは3通りの方法で処理できます。

  1. エラー
    • コンパイル時 / error: overflow of integer type 'u8' with value '256'
    • 実行時 / panic: integer overflow
  2. 値を増やさない (飽和演算) [3]
    • 演算子 +| による計算 / 255 +| 1 = 255
  3. 値を0に戻す (ラップアラウンド:wrapping)[4]
    • 演算子 +% による計算 / 255 +% 1 = 0

+|という演算子は、足し算の結果がオーバーフローするとき、結果を型で扱える最大の数値とします。u8なら255です。そして+%という演算子は足し算の結果がオーバーフローするとき、結果を計算上の結果と型で扱える最大値 + 1との差とします。u8なら計算上の結果と255 + 1との差となります。

const a: u8 = 255;
// エラー try stdout.print("a + 1 = {}\n", .{a + 1});
try stdout.print("a +| 1 = {}\n", .{a +| 1});
try stdout.print("a +% 1 = {}\n", .{a +% 1});

結果

a +| 1 = 255
a +% 1 = 0

逆に0 - 1の結果は(負の)オーバーフローとなります。-|の結果はオーバーフローするとき型で扱える最小値になり、-%の結果は型で扱える最大値 + 1と計算上の結果(負の数)との和となります。

const b: u8 = 0;
try stdout.print("{} -| 1 = {}\n", .{ b, b -| 1 });
try stdout.print("{} -% 1 = {}\n", .{ b, b -% 1 });

結果
0 -| 1 = 0
0 -% 1 = 255

符号あり整数の演算

ラップアラウンドは符号あり整数で行うときに注意が必要です。たとえば型がi8だと扱える範囲は-128から127となりますので、127 +% 1 = -128となり、-128 -% 1 = 127となるからです。なぜこのようになるのでしょうか。

const c: i8 = 127;
// c +% 1 = -128
const d: i8 = -128;
// d -% 1 = 127

負の数は、たとえば 1 + (-1) = 0 のように、絶対値が同じ正の数を足すと0になる値で表されます。

ビット幅が8のとき、1 = 0b00000001ですが、足して0となる値は0b11111111です。そのためビット幅が8のとき-1は0b11111111で表されます。ビット幅を無視すると両者の和は0b100000000ですが、左端(最上位)の1はビット幅を超えるオーバーフローのため、結果として1 + (-1)の値は0b00000000となるのです。

// ビット幅8の場合
1 = 0b00000001
-1 = 0b11111111
1 + (-1) = 0
0b00000001 + 0b11111111 = 0b00000000    // 左端(最上位)の1はオーバーフローのため現れない

ただ、この場合正の数は127までありますが128はありません。ですが-128は0b10000000で表します。これは最上位ビットが1のときは負の数を表すからです。全体では以下のようになります。

2進数 符号あり 符号なし
00000000 ~ 01111111 0 ~ 127 0 ~ 127
10000000 ~ 11111111 -128 ~ -1 128 ~ 255

この最上位ビットが1のときは負の数を表すという決まりは、ビットシフトを行う時にも注意が必要です。符号あり整数の右シフト(>>)では、最上位ビットの値を1桁右にずらした後、それと同じ値が最上位に設定されます。

// i8における正の数の右シフト
01111111 >> 0 = 01111111 = 127
01111111 >> 1 = 00111111 =  63
01111111 >> 2 = 00011111 =  31
01111111 >> 3 = 00001111 =  15
01111111 >> 4 = 00000111 =   7
01111111 >> 5 = 00000011 =   3
01111111 >> 6 = 00000001 =   1
01111111 >> 7 = 00000000 =   0

// i8における負の数の右シフト
10000000 >> 0 = 10000000 = -128
10000000 >> 1 = 11000000 =  -64
10000000 >> 2 = 11100000 =  -32
10000000 >> 3 = 11110000 =  -16
10000000 >> 4 = 11111000 =   -8
10000000 >> 5 = 11111100 =   -4
10000000 >> 6 = 11111110 =   -2
10000000 >> 7 = 11111111 =   -1

しかし左シフト(<<)では、最下位(右端)に0が追加され、それ以外はただ1桁左にずれるだけなので、最上位ビットの値が1か0かで大きく値が変化することがあります。

// 符号あり整数(i8)のとき
10101010 << 0 = 10101010 =  -86
10101010 << 1 = 01010100 =   84
10101010 << 2 = 10101000 =  -88
10101010 << 3 = 01010000 =   80
10101010 << 4 = 10100000 =  -96
10101010 << 5 = 01000000 =   64
10101010 << 6 = 10000000 = -128
10101010 << 7 = 00000000 =    0

// 符号なし整数(u8)のとき
10101010 << 0 = 10101010 =  170
10101010 << 1 = 01010100 =   84
10101010 << 2 = 10101000 =  168
10101010 << 3 = 01010000 =   80
10101010 << 4 = 10100000 =  160
10101010 << 5 = 01000000 =   64
10101010 << 6 = 10000000 =  128
10101010 << 7 = 00000000 =    0

飽和演算、ラップアラウンドの演算子

飽和演算、ラップアラウンドの演算子は+, -以外に単項の-(符号)、*(掛け算)、<<(左シフト)があります。[5]

演算 飽和演算 ラップアラウンド
+ +| +%
- (単項: -a) -%
- (2項: a - b) -| -%
* *| *%
<< <<|

まとめ

  • Zigではビット幅を で表現する
    • 整数型は符号あり、符号なしともにビット幅は0~65535まで(8, 16, 32, 64, 128のみドキュメントで言及)
    • 実数型のビット幅は16, 32, 64, 80, 128で仮数部と実数部に分かれている
    • 符号あり整数型と実数型には符号を表す1ビットがある
  • それぞれの型では扱える値の範囲が決まっている
    • u8なら0~255, i8なら-128~127など
    • 負の数は絶対値の値が同じ正の数との和が0になるように表される
      (ただし最上位ビットのみが1の場合はビット幅で表せる最小の負の数)
  • オーバーフローするときの処理はエラー、飽和演算、ラップアラウンドの3通りがある
    • 飽和演算は結果が型で扱える値を超えない
    • ラップアラウンドは結果が型で扱える範囲を超えた分を最大値 + 1との差(結果が負の数なら和)で表す
    • 符号あり整数ではラップアラウンドや左シフトに注意
    • 飽和演算、ラップアラウンドには+, -(単項、2項), *, <<の演算子が用意されている

変数の値を変更 (var, null, undefined) >
< 2進数とビット演算
ざっくりZig 一覧

脚注
  1. コンピュータ博物館 誕生と発展の歴史 ↩︎

  2. ドキュメント: Floats ↩︎

  3. ドキュメント Operators ↩︎

  4. ドキュメント Operators ↩︎

  5. ドキュメント Operators ↩︎

Discussion