浮動小数点についてちゃんと理解しておきたい
小数を含む計算機上の表現はいろいろあれど、現時点では特殊な用途を除けばIEEE754をつかっていると思います
ほとんどの方にとっては興味のないことかもしれませんが、ここでは数値表現についてちゃんと理解してみたい
という話です
(計算とか丸めとかについては、扱いません)
確認する処理系
byteレベルの処理から、実際に数値を出力して確認するのって結構めんどうなのですが、JavascriptのTypedArrayつかえばできるし簡単です
- Uint8Array(とか)
- Float64Array
ここではJavascriptのnumberにならって倍精度小数で例を記述していきます
内容はこちら
https://ja.wikipedia.org/wiki/%E5%80%8D%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0
これを理解できている人、できてしまう人にはこの記事は意味がありませんので、戻って他に有意義な時間を過してください
検算
Wikipediaの例をみて1,2,0とかあたりを出力してみます
ちゃんと意図した通りであれば、正しく答がでるはずです
なお処理系はLittle Endianです
const main = () => {
const arrayBuffer = new ArrayBuffer(8)
// const uint8 = new Uint8Array(arrayBuffer)
const uint16 = new Uint16Array(arrayBuffer)
const float64 = new Float64Array(arrayBuffer)
uint16.set([0,0,0,0x3ff0], 0)
//uint8.set([0,0,0,0 ,0,0,0xf0,0x3f],0)
console.log('result 1', float64.slice(0)[0])
uint16.set([0,0,0,0x4000], 0)
console.log('result 2', float64.slice(0)[0])
uint16.set([0,0,0,0], 0)
console.log('result 0', float64.slice(0)[0])
uint16.set([0x001,0,0,0x3ff0], 0)
console.log('result 1.0000000000000002', float64.slice(0)[0])
}
main()
結果
result 1 1
result 2 2
result 0 0
result 1.0000000000000002 1.0000000000000002
上記のように出力されていれば大丈夫そうです
定義
byteのLayoutはWikipediaにあるものが分かりやすいので、そちらを参照してください
最初の1bitが正負の符号
2-12bitsが指数部
13-64bitsまでが仮数部になります
また基数は2です
符号(sign)
最初の1bitsが0のときは正の数です
1のときは負の数です
指数部(exponent)
指数部は11bitsの長さがあります
基数は2なのでつまり2^12-1まで表現できます、がちょっと待ってください
整数だけなら2^0から2^2047でいいですが、浮動小数点ですので2^-1(=0.5)とかも表現できないといけません
ですので指数部を0として扱う数を0x3ff(1023)として定義しています
この数値のことをExcess-NとかOffset binaryとかいいます
なので0x3ffのときが2^0=1となり0x400が2^1=2とかなります
0x3feのときは2^-1=0.5となります
仮数部
基数、指数部、符号で2のN乗については小数も含めて表現できるようになりました
じゃあ「3」はどうするの?となります
これは2の指数表現をしたときの小数部分が仮数部になります
これなんかも
https://ja.wikipedia.org/wiki/IEEE_754
を読んでもらえればわかるのでそちらを参照する人はそれで問題ありません
ここではまず3をためしてみます
example 3
- 3は0b11
- 0b11は1.1 * (2^1)
- 指数部は2^1なのでExcessと足して0x4000
- 仮数はそのままbitを上位からならべるので1000000000000(52bit分)
- 最初の4bitを16進数になおすと0x8
- 符号は0
上記のことをあわせると
uint16.set([0,0,0,0x4008], 0) // 最初の12bit(符号+指数部) 0x4000(0b0100000000000000) + 仮数の4bit 0x8000(0b1000)
console.log('result 3', float64.slice(0)[0])
example -11
- 符号部は1
- 絶対値は11
- 絶対値を二進数にして0b1011
- 0b1011を2の指数表現にして1.011 * (2^3)
- 指数部は0x3ff + 3 = 0x402
- 仮数は0110なので0x6
- あわせると0b1010000000100110 -> 0xc026
uint16.set([0,0,0,0xc026], 0)
console.log('result -11', float64.slice(0)[0])
特殊な数
- 指数部が0x7ffのとき(すべてのbitが1のとき)は特別な数値になる
- 指数部が0x7ffのとき仮数が0でないならNaN
- 指数部が0x7ffで仮数が0のとき、符号が0ならInfinity, 1なら-Infinity
サンプルコード
const main = () => {
const arrayBuffer = new ArrayBuffer(8)
// const uint8 = new Uint8Array(arrayBuffer)
const uint16 = new Uint16Array(arrayBuffer)
const float64 = new Float64Array(arrayBuffer)
uint16.set([0,0,0,0x3ff0], 0)
//uint8.set([0,0,0,0 ,0,0,0xf0,0x3f],0) でもよい
console.log('result 1', float64.slice(0)[0])
uint16.set([0,0,0,0x4000], 0)
console.log('result 2', float64.slice(0)[0])
uint16.set([0,0,0,0], 0)
console.log('result 0', float64.slice(0)[0])
uint16.set([0x0001,0,0,0x3ff0], 0)
console.log('result 1.0000000000000002', float64.slice(0)[0])
uint16.set([0,0,0,0x7ff0], 0)
console.log('result Infinity', float64.slice(0)[0])
uint16.set([0,0,0,0xfff0], 0)
console.log('result -Infinity', float64.slice(0)[0])
uint16.set([0x1,0,0,0x7ff0], 0)
console.log('result NaN', float64.slice(0)[0])
uint16.set([0x1,0,0,0xfff0], 0)
console.log('result NaN', float64.slice(0)[0])
uint16.set([0xffff,0xffff,0xffff,0x7fef], 0)
console.log('result MAX_VALUE', float64.slice(0)[0])
console.log('MAX_VALUE definition', Number.MAX_VALUE)
uint16.set([0xffff,0xffff,0xffff,0xffef], 0)
console.log('result MIN_VALUE', float64.slice(0)[0])
console.log('MIN_VALUE definition', Number.MIN_VALUE)
uint16.set([0,0,0,0x3fe0], 0)
console.log('result 0.5', float64.slice(0)[0])
uint16.set([0,0,0,0x4008], 0)
console.log('result 3', float64.slice(0)[0])
uint16.set([0,0,0,0x4010], 0)
console.log('result 4', float64.slice(0)[0])
uint16.set([0,0,0,0x4011], 0)
console.log('result 4.25', float64.slice(0)[0])
uint16.set([0,0,0,0x4014], 0)
console.log('result 5', float64.slice(0)[0])
uint16.set([0,0,0,0x4026], 0)
console.log('result 11', float64.slice(0)[0])
uint16.set([0,0,0,0x4008], 0)
console.log('result 3', float64.slice(0)[0])
uint16.set([0,0,0,0xc026], 0)
console.log('result -11', float64.slice(0)[0])
}
main()
まとめ
- IEEE 754は符号と2の指数表現と残りの仮数による表現
- 小数を表現するために指数には負が必要になるためExcess-Nだけ0となる値をずらすことで小数について表現するようにしている
- 倍精度浮動小数(64bit)では符号1bit, 指数部が11bit, 仮数部が52bit, Excess-Nが1023(0x3ff)の計64bitによる表現となる