esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

浮動小数点数のバイナリ表現を10進数表記へ変換してみる

こんにちは、星野源です。すみません取り乱しました、はたけやまです。

最近は趣味でCPUを自作しています。自作のCPUを浮動小数点演算に対応させるためにオープンソースのFPUコア(浮動小数点演算装置)の使い方を調べていたのですが、FPUとやりとりするデータが浮動小数点数のビット列なので、返ってきた結果が正しいのか正しくないのかがパッと見で分からなくて生きるのが辛い...( 浮動小数点数の 0x3F800000 が10進数表記でいくつか分かります?答えは 1.0です)

そこで、浮動小数点数のビット列を10進数表記へ変換するスクリプトを書いてみました。

f:id:htkymtks:20210520234418p:plain (↑ FPUとのデータのやりとり)

浮動小数点数の構造

変換スクリプトを書く前に浮動小数点数について軽く説明を。「浮動小数点数」は小数点を含む数値をビット列にエンコードする表現方式で、現在では「IEEE 754」として標準化されたものが広く使われています。IEEE 754で定義されている浮動小数点数には32ビットの単精度浮動小数点数(single)や64ビットの倍精度浮動小数点数(double)などがあります。

32ビットの単精度浮動小数点数は以下のような内部構造を持ちます。

  • 符号部(sign)1ビット
  • 指数部(exponent)8ビット
  • 仮数部(fraction)23ビット

f:id:htkymtks:20210520235020p:plain ( 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) 指数部

(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 によりワンライナーに!!!

packとunpackに向き合ってみた

Rubyでバイナリアンを目指すためにpackとunpackに向き合った結果、僕にもワンライナーできたよー!!!ビッグエンディアンな単精度浮動小数点数としてunpackする必要があるのが罠でした。

gist1416977adf7d370eced64d22e0c273d2

参考