コンパイラ作成(76) 浮動小数点数

今回はお勉強

char型のサポートが粗方終わったんで、次に何やるかって考えたよ。最初はshort型にしようかと思ったんだけど、やることはchar型と大体同じで詰まらないから、double型に手を出してみるよ。今まで浮動小数点数を使ったプログラミングはあんまりしてこなかったし、特にアセンブリレベルではほとんど知識はないよ。ってことでお勉強するよ。

IEEE754

まずはIEEE 754 - Wikipediaから始めるよ。単精度ってのと倍精度ってのがC言語のfloat型とdouble型だよね。単精度が32bit、倍精度が64bitだね。なるほどなるほど。この内、決められたbitを指数を表すのに使うんだよね。これは何となく知ってたよ。単精度の場合、指数部が8bit、仮数部が23bitか。それと正負を表す符号用の1bit。全部合わせて32bitか。ふむふむ。良く読んでいくとけち表現ってのが出てくるね。仮数部の先頭が必ず1になるんで、それを省いて1bit儲けるってことか。これは知らなかったよ。
C言語にはもう一つlong double型ってのもあるよね。これはなんじゃらほいって見て行ったら、拡張倍精度浮動小数点数 - Wikipediaに詳しく乗ってるね。ようはx87の内部形式ってことで80bitなのか。調べてみたらコンパイラでのサポートもまちまちみたいだ。gccやclangはサポートしてるんだね。それもターゲットがx86の場合だけ見たい。他のCPUだと扱えないだろうしね。long double - Wikipediaにも目を通しといたよ。

mycでのサポート

浮動小数点数をサポートするのに何が必要か整理してみたよ。

  1. Lexerクラスでのサポート
  2. IEEE754内部形式への変換
  3. 浮動小数点数の計算方法

最初のLexerクラスの件はやればできるかな。二番目の件は難しそう。三番目はx86_64のレジスタ構成、命令セットを一から勉強しないといけないなあ。

IEEE754内部形式への変換

まずはこれを調べてみるよ。さてどうやって変換するかだな。

  1. Rubyに頼る
  2. 自力で頑張る
  3. 他の言語に頼る

一番簡単そうなのはRubyにお願いする方法だけど、Rubyはlong double型をサポートしてないんだよね。自力で頑張るってのは技術的には面白いけど大変そうだなあ。他の言語、例えばC言語に頼るってのもあるよね。C言語ならstrtoldとかで簡単に変換できるよね。ただこれやるにはRubyからC言語のルーチンを呼び出す方法を調べないといけないなあ。

テストプログラム

ちょっと試してみるよ。

#! /bin/sh
exec ruby -S -x "$0" "$@"
#! ruby

#
# ieee754
#

def ieee754_binary(n,format=:double)
  a = n.split('.')
  integer = a[0]
  if a.size < 2 then
    decimal = ""
  else
    decimal = a[1]
  end

  b_integer = integer.to_i.to_s(2)
  b_decimal = ""
  if format == :float then
    mantissa_bits = 23
    exponent_bits = 8
    exponent_bias = 127
  elsif format == :double then
    mantissa_bits = 52
    exponent_bits = 11
    exponent_bias = 1023
  elsif format == :quad then
    mantissa_bits = 112
    exponent_bits = 15
    exponent_bias = 16383
  elsif format == :fp80 then
    mantissa_bits = 63
    exponent_bits = 15
    exponent_bias = 16383
  end
  y = 5
  i = 0
  x = 0
  loop do
    x = x * 10
    if i < decimal.size then x += decimal[i].to_i end
    if x >= y then
      b_decimal += "1"
      x = x - y
    else
      b_decimal += "0"
    end
    y = y * 10 / 2
    i = i + 1
    if i >= mantissa_bits then break end
  end
  mantissa = (b_integer.insert(1,".") + b_decimal)[2,mantissa_bits]
  if a[0] == "0" then
    exponent = (exponent_bias - 2).to_s(2).rjust(exponent_bits,"0")
  else
    exponent = (b_integer.size-1 + exponent_bias - 1).to_s(2).rjust(exponent_bits,"0")
  end
  if format == :fp80 then
    ieee754 = "0"+exponent+"1"+mantissa
  else
    ieee754 = "0"+exponent+mantissa
  end
=begin
  p b_integer,b_decimal
  p mantissa,exponent
  p ieee754
=end
  return ieee754
end

n = ARGV[0]
puts "自力で変換"
puts "ieee754 float  = " + ieee754_binary(n,:float ).to_i(2).to_s(16)
puts "ieee754 double = " + ieee754_binary(n,:double).to_i(2).to_s(16)
puts "ieee754 quad   = " + ieee754_binary(n,:quad  ).to_i(2).to_s(16)
puts "x87     fp80   = " + ieee754_binary(n,:fp80  ).to_i(2).to_s(16)
=begin
fp80 = ieee754_binary(n,:fp80  )
p fp80[0, 16].to_i(2).to_s(16)
p fp80[16,64].to_i(2).to_s(16)
=end
puts
puts "Rubyで変換"
puts "ieee754 float  = " + [n.to_f].pack('g').bytes.map{|n| "%08b" % n}.join.to_i(2).to_s(16)
puts "ieee754 double = " + [n.to_f].pack('G').bytes.map{|n| "%08b" % n}.join.to_i(2).to_s(16)

自力で変換するのとRubyに頼るのをちょろちょろっと書いてみたよ。あってるのかどうか良く分からないけど動かしてみる。

~/ruby$ ./decimal.rb 10.78
自力で変換
ieee754 float  = 412c7ae1
ieee754 double = 40258f5c28f5c28f
ieee754 quad   = 400258f5c28f5c28f5c28f5c28f5c28f
x87     fp80   = 4002ac7ae147ae147ae1

Rubyで変換
ieee754 float  = 412c7ae1
ieee754 double = 40258f5c28f5c28f
~/ruby$

お、できたかな。自力での計算とRubyでの計算結果が一致した。もういっちょやってみるよ。

~/ruby$ ./decimal.rb 0.13
自力で変換
ieee754 float  = 3e90a3d7
ieee754 double = 3fd2147ae147ae14
ieee754 quad   = 3ffd2147ae147ae147ae147ae147ae14
x87     fp80   = 3ffd90a3d70a3d70a3d7

Rubyで変換
ieee754 float  = 3e051eb8
ieee754 double = 3fc0a3d70a3d70a4
~/ruby$

ありゃ駄目じゃん。やっぱりちょろちょろってやったんじゃ無理だったか。あえなく挫折。どっかの計算がバグってるんだろうけど、今日はもう頭が働かないから諦めよう。バグ取りできなかったらRubyからC言語を呼び出す方法を調べよう。それも面倒くさくなったらlong double型のことは一旦諦めよう。