BK通信 - 数値のバッドノウハウ

最終更新日: 2008-04-23

WEB+DB PRESS Vol. 45 に向けて書いた記事の元の原稿です。


ソフトウェアなどを使いこなすために、ストレスを感じながらもしぶしぶ覚えなければならないようなノウハウ、「バッドノウハウ」がテーマの本連載、第2回の今回は数値に関するバッドノウハウ (以下 BK) を取り上げたいと思います。

JavaScript の parseInt 関数

JavaScript には、文字列を整数に変換する組み込みの関数 parseInt があります。この関数は、第1引き数に文字列、第2引き数に基数を渡して使うのが基本です。しかし、基数を省略した場合は、文字列の中身に応じて自動的に基数が選ばれます。

その結果、"08" が8進数として解釈されて 0 になる (8 は8進数では無効な値)、という厄介な挙動が発生します。

// Firefox 2, IE 7 ともに 0 が表示される
alert(parseInt("08"));

この挙動は、2桁の数字で入力された月や日を処理するプログラムでよく問題になります。「JavaScript parseInt」で検索すると "08" が 8 進数として解釈されてはまった、というページが多数見つかります。典型的な落とし穴です。

ECMAスクリプトの仕様書 *1を読むと、 parseInt 関数は基数が与えられず (または基数が0)、文字列の先頭が 0x または 0X の場合は、文字列を 16進数として解釈すると明記されています。しかし、文字列の先頭が 0 の場合の解釈は実装依存であり、8進数として解釈しても構わないが、10進数として解釈するのが望ましいと書かれています。

このようなちょっとした「賢い」挙動が思わぬ落とし穴を生むというのはよくあるパターンです。ソフトウェアを設計する上で、気をつけなければならない点だと思います。

8進数なんて使わねーし
8進数なんて使わねーし

この parseInt の挙動、何か似たようなものがあったような、と思っていたところ、 C 言語の strtol 関数が似たような仕様であることを思い出しました。

strtol は文字列を long 型の整数に変換する関数です。基数に 0 を与えた場合、文字列の中身に応じて自動的に基数が選ばれます。そして、文字列が 0x または 0X から始まっていれば 16進数が、0 から始まっていれば 8 進数が選ばれます。

printf の %g

先日、ちょっとした統計を集計するスクリプトを書いて実行したところ、まったく予想外の結果が出て驚きました。たとえていえば、人口の最も多い都道府県は鳥取県*2 という結果が得られたような感じです。

考えられる可能性としては、以下のようなものがあります。

  1. 予想外の結果だが実は正しい
  2. 集計するスクリプトにバグがある
  3. 集計する前の段階でデータが間違っている

まず 2 を疑ってみましたが、単純に整数を足し合わせているだけのプログラムなのでバグがあるようには見えません。次に 3の可能性を探ってみると、元のデータは printf の %g というフォーマットで出力されていることがわかりました。

%g は浮動小数点の数値のフォーマットに用いられます。同じく浮動小数点に用いられる %f と違い、 g は以下のような賢い動作をします。

  1. 小数点以下に 0 が続く場合は省略する (123.00 -> 123)
  2. 数値がある程度以上大きい場合は、科学表記を用いる (1000000 -> 1e+06)

というわけで、私が整数だと思って足し合わせていた数値は実は浮動小数点だったのでした。たまたま私がサンプルとして見ていた数字が 1 の仕様によって整数のように見えていたため、データは整数だと思い込んでしまったのが敗因です。

集計スクリプトの中では Ruby の to_i メソッドを使って文字列を整数に変換していたため、科学表記の数値がくると、eの前の数字だけが整数として解釈されていました。1e+06 が 1000000 ではなく 1 としてカウントされてしまうのですから、結果が予想とまったく異なっていたのは当然です。to_i の代わりに to_f を使って解決しました。

科学表記なんて想定してないし
科学表記なんて想定してないし

ちなみに、 Ruby の to_i は基数が省略された場合は、文字列を10 進数として解釈します。文字列の中身に応じて 16進数や8進数として解釈するといったことはありません。

x86 の浮動小数点演算

先日、あるプログラムを Mac OS X の gcc でビルドして実行したところ、同じプログラムを Linux の gcc でビルドしたときと結果が微妙に異なることに気づきました。

結果をみると、どうも浮動小数点演算の微妙に結果が異なっているようです。原因としてまず思い当たったのは x86 の浮動小数点演算命令です。

インテルの x86 の浮動小数点演算は 80ビットレジスタを使うため、他のプロセッサと結果が微妙に異なることがある、ということが知られています *3

しかし、今回の場合、どちらもハードウェアはインテルの x86 系のプロセッサを積んでいます。なのになぜ結果が違うのかと思って調べてみると、 Mac OS X Leopard の gcc はデフォルトで浮動小数点演算に SSE 命令*4を使っていることがわかりました。SSE 命令を使った場合、 x86 伝統の 80ビットレジスタは用いられないため、80ビットレジスタに起因する問題は起きません。

このときは結局、 Linux でビルドしたバイナリと同じ結果が欲しかったのでコンパイラオプションに -mno-sse (SSE命令を禁止) を追加してごまかしました。

結果違わないで欲しいし
結果違わないで欲しいし

以前にも別の場面で x86 の浮動小数点演算の挙動ではまって、ブログに書いたことがあります*5。x86 の浮動小数点演算の挙動はなかなか厄介な問題です。

まとめ

今回は数値に関する BK を3つ紹介しました。最初の parseInt のものは、わかってしまえば「なんだ 8進数かよ!」で済む簡単な問題ですが、最後の x86 の浮動小数点演算の問題は、なぜ結果が異なるのか理解するのはなかなか難しい、厄介な問題です。BKとひとことで片付けるにはもったいない奥が深いテーマといえそうです。


*1<http://www.mozilla.org/js/language/E262-3.pdf>
*2人口が最も都道府県
*3詳しい解説はオライリージャパン刊『Binary Hacks』の「Hack #98 x86が持つ浮動小数点演算命令の特殊性」にあります。
*4Streaming SIMD Extensions の略。インテルが開発した高速な浮動小数点演算のための命令セット。
*5浮動小数点演算ではまった話 - bk ブログ <http://0xcc.net/blog/archives/000164.html>