ソフトウェアなどを使いこなすために、ストレスを感じながらもしぶしぶ覚えなければならないようなノウハウ、「 バッドノウハウ」( BadKhowhow)がテーマの本連載、第2回の今回は数値に関するバッドノウハウ(以下BK)を取り上げたいと思います。
JavaScriptのparseInt関数
JavaScriptには、文字列を整数に変換する組み込みの関数parseInt があります。この関数は、第1引数に文字列、第2引数に基数を渡して使うのが基本です。しかし、基数を省略した場合は、文字列の中身に応じて自動的に基数が選ばれます。
その結果、"08"が8進数として解釈されて0(ゼロ)になる(8は8進数では無効な値) 、という厄介な挙動が発生します(リスト1 ) 。
リスト1 JavaScriptのparseInt関数
// Firefox 2、IE 7ともに0が表示される
alert(parseInt("08"));
この挙動は、2桁の数字で入力された月や日を処理するプログラ
ムでよく問題になります。「 JavaScript parseInt」で検索すると"
08"が8進数として解釈されてはまった、というページが多数見つか
ります。典型的な落とし穴です。
ECMAスクリプトの仕様書
を読むと、parseInt関数は基数が与えられず(または基数が0) 、
文字列の先頭が0xまたは0Xの場合は、文字列を16進数として解釈す
ると明記されています。しかし、文字列の先頭が0の場合の解釈は
実装依存であり、8進数として解釈してもかまわないが、10進数と
して解釈するのが望ましいと書かれています。
このような
ちょっとした「賢い」挙動が思わぬ落とし穴を生む、というのはよ
くあるパターンです。ソフトウェアを設計する上で、気をつけなけ
ればならない点だと思います。
「このparseIntの挙動、何か似たようなものがあったような」と思っていたところ、C言語のstrtol関数が似たような仕様(文字列の先頭で基数が変わる)であることを思い出しました。parseInt()の祖先はこいつかもしれません。
printfの%g
先日、ちょっとした統計を集計するスクリプトを書いて実行したところ、予想外の結果が出て驚きました。たとえていえば、人口の最も多い都道府県は鳥取県[1] という結果が得られたような感じです。
考えられる可能性としては、図1 のようなものがあります。
図1 考えられる可能性
① 予想外の結果だが実は正しい
② 集計するスクリプトにバグがある
③ 集計する前の段階でデータが間違っている
まず②を疑ってみましたが、単純に整数を足し合わせているだけのプログラムなのでバグがあるようには見えません。次に③の可能性を探ってみると、元のデータはprintf の%g というフォーマットで出力されていることがわかりました。
%gは浮動小数点の数値のフォーマットに用いられます。同じく浮動小数点に用いられる%fと違い、%gは図2 のような賢い動作をします。というわけで、筆者が整数だと思って足し合わせていた数値は実は浮動小数点だったのでした。たまたま筆者がサンプルとして見ていた数字が[1]の仕様によって整数のように見えていたため、データは整数だと思い込んでしまったのが敗因です。
図2 %gの動作
[1] 小数点以下に0が続く場合は省略する
例:123.00 -> 123
[2] 数値がある程度以上大きい場合は、科学表記を用いる
例:1000000 -> 1e+06
集計スクリプトの中ではRubyのto_i メソッドを使って文字列を整数に変換していたため、科学表記の数値がくると「e」の前の数字だけが整数として解釈されていました。「 1e+06」が1000000ではなく1としてカウントされてしまうのですから、結果が予想とまったく異なっていたのは当然です。to_iの代わりにto_fを使って解決しました。
ちなみに、Rubyのto_iは基数が省略された場合は、文字列を10進数として解釈します。文字列の中身に応じて16進数や8進数として解釈するといったことはありません。
x86の浮動小数点演算
先日、あるプログラムをMac OS X(v10.5、Leopard)のgccでビルドして実行したところ、同じプログラムをLinuxのgccでビルドしたときと結果が微妙に異なることに気づきました。
結果を見ると、どうも浮動小数点演算の挙動が微妙に異なっているようです。原因としてまず思い当たったのは、x86の浮動小数点演算命令 です。Intelのx86の浮動小数点演算は80ビットレジスタを使うため、他のプロセッサと結果が微妙に異なることがある、ということが知られています[2] 。
しかし、今回の場合、どちらもハードウェアはIntelのx86系のプロセッサを積んでいます。なのになぜ結果が違うのかと思って調べてみると、Mac OS X Leopardのgccはデフォルトで浮動小数点演算にSSE命令 [3] を使っていることがわかりました。SSE命令を使った場合、x86伝統の80ビットレジスタは用いられないため、80ビットレジスタに起因する問題は起きません。
このときは結局、Linuxでビルドしたバイナリと同じ結果が欲しかったのでコンパイラオプションに-mno-sse(SSE命令を禁止)を追加してごまかしました。
以前にも別の場面でx86の浮動小数点演算の挙動ではまって、ブログに書いたことがあります[4] 。x86の浮動小数点演算の挙動はなかなか厄介な問題です。
[2] 『Binary Hacks』( 高林 哲/鵜飼 文敏/佐藤 祐介/浜地 慎一郎/首藤 一幸著、オライリージャパン、2006)の「Hack #98:x86が持つ浮動小数点演算命令の特殊性」( p.366)に詳しい説明があります。
まとめ
今回は数値に関するBKを3つ紹介しました。最初のparseIntのものは、わかってしまえば「なんだ8進数かよ!」で済む簡単な問題ですが、最後のx86の浮動小数点演算の問題は、なぜ結果が異なるのか理解するのはなかなか難しい、厄介な問題です。BKと一言で片付けるにはもったいない奥が深いテーマといえそうです。