Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

JSON::XSで作られる浮動小数点数でハマった話

JSON::XSを使うと、小数は浮動小数点数にパースしてくれる。

use JSON::XS qw(decode_json);

my ($double) = @{decode_json("[0.6]")};

この値、printすると "0.6" なのだけど、0.6とイコールではない。

print $double, "\n"; # 0.6
print +($double - 0.6), "\n"; # 1.11022302462516e-16

なので、直感と違う振る舞いをしたりする。

# 3 / 0.6 == 5
printf "%s / %s == %s\n", 3,     0.6, int(3 /     0.6);

# 3 / 0.6 == 4 <- unexpected :(
printf "%s / %s == %s\n", 3, $double, int(3 / $double); 

仕方ないので端数を四捨五入して回避。

$double = int($double * 100 + .5) / 100;

# 3 / 0.6 == 5
printf "%s / %s == %s\n", 3, $double, int(3 / $double); 

こんな手もありそう。

$double = "$double" + 0;

# 3 / 0.6 == 5
printf "%s / %s == %s\n", 3, $double, int(3 / $double); 
余談

RTには@__kan さんが上げてるけど、仕様と言えば仕様な気もする。Dumpしてみたけど違いはわからず。

use Devel::Peek;
Dump(0.6);
# SV = NV(0x7fd549839808) at 0x7fd54982a488
#   REFCNT = 1
#   FLAGS = (NOK,READONLY,pNOK)
#   NV = 0.6

Dump($double);
# SV = NV(0x7fd549839810) at 0x7fd54982a440
#   REFCNT = 1
#   FLAGS = (PADMY,NOK,pNOK)
#   NV = 0.6
(10/24 追記)環境とか

再現しない @maka2_donzoko さんとかいらっしゃるみたいなので一応環境を。以下の環境では現象が再現。

% perl -V
Summary of my perl5 (revision 5 version 12 subversion 2) configuration:

  Platform:
    osname=darwin, osvers=11.4.0, archname=darwin-2level
    uname='darwin ... 11.4.0 darwin kernel version 11.4.0: mon apr 9 19:32:15 pdt 2012; root:xnu-1699.26.8~1release_x86_64 x86_64 '
    config_args='-de -Dprefix=...'
    hint=recommended, useposix=true, d_sigaction=define
    useithreads=undef, usemultiplicity=undef
    useperlio=define, d_sfio=undef, uselargefiles=define, usesocks=undef
    use64bitint=define, use64bitall=define, uselongdouble=undef
    usemymalloc=n, bincompat5005=undef
  Compiler:
    cc='cc', ccflags ='-fno-common -DPERL_DARWIN -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include',
    optimize='-O3',
    cppflags='-fno-common -DPERL_DARWIN -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include'
    ccversion='', gccversion='4.2.1 Compatible Apple Clang 4.0 ((tags/Apple/clang-421.0.57))', gccosandvers=''
    intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=12345678
    d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
    ivtype='long', ivsize=8, nvtype='double', nvsize=8, Off_t='off_t', lseeksize=8
    alignbytes=8, prototype=define
...以下略...

後、configで-Duselongdoubleとかつかても変わるかもってIRCで流れてた。さらに @syohex さんが情報くれた。

i386 Linux環境でJSON::XSを makeするときCFLAGSに"-msse2 -mfpmath=sse"をつけると再現しました。80bitの拡張浮動小数点を使うと再現しないので精度の問題だと思います。

@syohex

JSON::XSの独自実装の atofを Perl本体のものを利用するように変更したところ、手元では期待どおり動きました。自前の atofがいまいちだったというのが結論でしょうか. https://gist.github.com/3944470

@syohex

(10/25 追記)改めて問題点を整理

@tokuhirom さんにはてブもらいましたが、問題点は以下に尽きますね。

printf("%.20f\n", $dat);
printf("%.20f\n", 0.6);
0.60000000000000008882
0.59999999999999997780

Floating point number decoding in JSON::XS

要は単に、処理系と独自実装のデコード結果が一致するか否かってのが問題でした。