こんにちは、星野源です。すみません取り乱しました、はたけやまです。
最近は趣味でCPUを自作しています。自作のCPUを浮動小数点演算に対応させるためにオープンソースのFPUコア(浮動小数点演算装置)の使い方を調べていたのですが、FPUとやりとりするデータが浮動小数点数のビット列なので、返ってきた結果が正しいのか正しくないのかがパッと見で分からなくて生きるのが辛い...( 浮動小数点数の 0x3F800000 が10進数表記でいくつか分かります?答えは 1.0です)
そこで、浮動小数点数のビット列を10進数表記へ変換するスクリプトを書いてみました。
(↑ FPUとのデータのやりとり)
浮動小数点数の構造
変換スクリプトを書く前に浮動小数点数について軽く説明を。「浮動小数点数」は小数点を含む数値をビット列にエンコードする表現方式で、現在では「IEEE 754」として標準化されたものが広く使われています。IEEE 754で定義されている浮動小数点数には32ビットの単精度浮動小数点数(single)や64ビットの倍精度浮動小数点数(double)などがあります。
32ビットの単精度浮動小数点数は以下のような内部構造を持ちます。
- 符号部(sign)1ビット
- 指数部(exponent)8ビット
- 仮数部(fraction)23ビット
( File:Float example.svg - Wikimedia Commons )
手作業で浮動小数点数を求める
浮動小数点数への理解を深めるため、10進数の「-118.625」を例にして符号部、指数部、仮数部に入る値を計算してみます。
(1) 符号部
- 符号部には、正の数の場合は0が、負の数の場合は1が入る
- -118.625は負の数なので、「符号部 = 1」となる
- (符号を求めた後は、-118.625 ではなく118.625 で後続の計算を行う)
(2) 10進数を2進数に変換する
- 118.625を2進数に変換します
- まずは整数部の118と小数部の0.625に分ける
- 整数部
- 118 = (64 * 1) + (32 * 1) + (16 * 1) + (8 * 0) + (4 * 1) + (2 * 1) + (1 * 0)
- 10進数の「118」を2進数にすると「1110110」となる
- 整数部の2進数表記 = 1110110
- 小数部
- 0.625 = (0.5 * 1) + (0.25 * 0) + (0.125 * 1)
- 10進数の「0.625」を2進数にすると「0.101」となる
- 小数部の2進数表記 = 0.101
- 整数部 + 小数部
- 1110110 + 0.101 = 1110110.101
- 118.625の2進数表記 = 1110110.101
(3) 求めた2進数を指数表記へ変換
- 1110110.101 = 1.110110101 * (2 ^ 6)
- 「仮数 = 1.110110101」「指数 = 6」となる
(4) 指数部
- (3) で求めた指数6にバイアス値の127を加えて、「指数部 = 6 + 127 = 133(10進数)= 10000101(2進数)」となる
(5) 仮数部
- (3) で求めた仮数1.110110101のうち、仮数の先頭は常に「1.」となるので省略し、仮数部23ビットに足りない分はうしろに0を詰めて、「仮数部 = 11011010100000000000000」となる
(6) 符号 + 指数部 + 仮数部
- 符号「1」+ 指数部「10000101」+ 仮数部「11011010100000000000000」
- 「-118.625(10進数)」の32ビット単精度浮動小数点数の表記は「11000010111011010100000000000000」となる
以下のサイトを使って、計算した浮動小数点数の答え合わせしてみましょう。
- 浮動小数点数内部表現シミュレーター
手作業で求めた計算結果が「11000010111011010100000000000000」で、シミュレータで計算した結果が「11000010111011010100000000000000」。どうやら合ってそうです。
「浮動小数点数のバイナリ表現→10進数」変換スクリプト
32ビットの単精度浮動小数点数への理解が深まったので、さっそくスクリプトを書いてみます。
# bin2float.rb # 32ビット単精度浮動小数点数の正式名称は「binary32」 class Binary32 def initialize(bin32_str) # セパレータ `_` を取り除く str = bin32_str.gsub(/_/, '') if str.size == 8 # 16進数表記の場合は2進数表記へ変換 str = "%032b" % str.to_i(16) end raise 'Invalid Binary32 string' unless str.size == 32 # 0バイト目は符号(+ or -) @sign = str[0] # 1〜8バイト目は指数部 @exp = Exponent8.new(str[1..8]) # 9〜32バイト目は仮数部 @fract = Fraction23.new(str[9..]) end def to_f # ゼロ return 0 if @exp.to_i == 0 && @fract.to_f == 0 # 非正規数 return Float::MIN if @exp.to_i == 0 && @fract.to_f != 0 # 無限大 return Float::INFINITY if @exp.to_i == 255 && @fract.to_f == 0 # NaN return Float::NAN if @exp.to_i == 255 && @fract.to_f != 0 # 符号が0ならプラスの値、1ならマイナス sign = @sign == "0" ? 1 : -1 # 仮数部を求める際に省略した1.0を戻す fract = @fract.to_f + 1 # 指数部を求める際に足したバイアス(127)を引く exp = @exp.to_i - 127 sign * fract * (2 ** exp) end end # 指数部の8bit class Exponent8 def initialize(exp_str) @exp = exp_str end def to_i @exp.to_i(2) end end # 仮数部の23bit class Fraction23 def initialize(fract_str) @fract = fract_str end def to_f # (b0 * 0.5) + (b1 * 0.25) + (b2 * 0.125) + (b3 * 0.0625) + ... @fract.split(//).map(&:to_i).zip(1..23).map {|b, n| b * (2 ** (n * -1)).to_f }.sum end end p Binary32.new(ARGV[0]).to_f
使い方はこんな感じ。先ほど手で計算した「-118.625」も正しく計算できてそうです。
# 正の数 $ ruby bin2float.rb 01000010111011010100000000000000 118.625 # 負の数 $ ruby bin2float.rb 11000010111011010100000000000000 -118.625 # ゼロ $ ruby bin2float.rb 00000000000000000000000000000000 0 # 非正規化数(0ではないけれどBinary32で表現できないぐらい小さい値) $ ruby bin2float.rb 00000000010000000000000000000000 2.2250738585072014e-308(Float::MINが返る) # 無限大 $ ruby bin2float.rb 01111111100000000000000000000000 Infinity # NaN $ ruby bin2float.rb 01111111100000000000000000000001 NaN # アンダーバーで区切ってもOK $ ruby bin2float.rb 0_10000101_11011010100000000000000 118.625 # 16進数表記もOK $ ruby bin2float.rb 42ED4000 118.625
突然のワンライナー
「わーい、できたー」と今回書いたスクリプトをTwitterへ投稿したところ id:udzura によりワンライナーに!!!
これ、Ruby使えるならunpack使うと短くなりそう...https://t.co/Nv6TCmSFXO
— Uchio Kondo 🍙 (@udzura) 2020年9月28日
s = eval("0b" + ARGV[0])
— Uchio Kondo 🍙 (@udzura) 2020年9月28日
p (0..3).map{|i| ((s & (0xff << i*8)) >> i*8).chr }.join.unpack('f')
2進数ならこれでいけそう。非正規化数がなんか違うけど...
今まで避け続けていたpackとunpackに向き合う日がついに来てしまった...
— はたけやまたかし (@htkymtks) 2020年9月28日
Rubyでバイナリアンを目指すなら絶対使いこなせる方がいいと思います!
— Uchio Kondo 🍙 (@udzura) 2020年9月28日
packとunpackに向き合ってみた
Rubyでバイナリアンを目指すためにpackとunpackに向き合った結果、僕にもワンライナーできたよー!!!ビッグエンディアンな単精度浮動小数点数としてunpackする必要があるのが罠でした。
gist1416977adf7d370eced64d22e0c273d2