Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 | 

2014年8月9日(土) PHP 5.4.4から==の挙動が一段と難しくなりました このエントリーを含むブックマーク このエントリーのブックマークコメント

PHPの==は両辺を適当に型キャストしてから比較するような演算子です。この型キャストの規則は難解すぎる上にドキュメントも不十分なため、PHPプログラマでも完璧に理解している人はほとんど居ないくらいの印象です。バグの原因になりかねないため、なるべく==を使わないようにしているPHPプログラマも多いはずです。


ところで、この==演算子の挙動がPHP 5.4.4から変更されていることはあまり知られていません。本稿ではこの内容を紹介します。


Bug #54547 の騒動

まずはこの仕様変更の経緯を紹介します。


2年ほど昔、Hacker News2^63付近の整数に対応する文字列をPHPで比較したときの挙動がおかしいというスレッドが盛り上がったことがありました。具体的には、PHPでは「'9223372036854775807' == '9223372036854775808'」がtrueになるという話題で、PHPの仕様をDISるような流れでした。


僕のように古くからのPHPユーザーにとっては==の挙動がキモいことは常識ですし、この例で浮動小数点数比較されているのも一目瞭然、他の言語の人が叩きに来るのも日本ではよくあることで、海外も周回遅れで同じ流れになってるのかなーというくらいの印象でした。僕からするとこれは仕様ですし、==の実装は複雑すぎて整合性を保ちつつ不満を解消するような仕様変更は不可能だと感じていたため、いくら叩かれようが修正はありえないと思っていました。


ところがPHP中の人がムシャクシャしていたのか何なのかはわかりませんが、これをバグとして修正する流れになりました。そのバグ報告と修正までの流れは「PHP :: Bug #54547 :: wrong equality of string numbers」で確認することができます。これがPHP 5.4.4から取り込まれており、今回紹介する内容ということになります。


PHP 5.4.4での変更内容

PHP 5.4.4からは、上で紹介した'9223372036854775807' == '9223372036854775808'」がfalseを返すようになっています。


この変更の詳細についてマニュアルなどには記述が見当たりませんが、ソースパッケージの./UPGRADINGに次のように書いてあります。


Long numeric strings that do not fit in integer or double (such as

"92233720368547758070") are compared using string comparison if

they could otherwise result in precision loss - since 5.4.4.


翻訳すると次のようになります。


整数浮動小数点数におさまらないような巨大な数値文字列("92233720368547758070"など)を比較する場合に、

文字列比較しないと精度が落ちるような場合には文字列比較が使われます。(PHP5.4.4から)


そもそも言葉足らずなのですが、これは数値文字列同士を比較する場合の話題です。PHP 5.4.3以前では数値文字列同士を比較する場合はかならず整数または浮動小数点数にキャストされてから比較されていました。しかし、キャストで精度が落ちるような数値文字列同士の場合は文字列比較するよ、というのがPHP 5.4.4での変更ということになります。


説明のため、64bit環境のPHP 5.5.11での実行例を示します。


<?php
$x1 = "9223372036854775807";
$y1 = "0X7FFFFFFFFFFFFFFF";
var_dump($x1==$y1); // bool(true), 数値比較している(文字列比較していたらfalseのはず)

$x2 = "9223372036854775808";
$y2 = "0X8000000000000000";
var_dump($x2==$y2); // bool(false), 文字列比較している(数値比較していたらtrueのはず, PHP 5.4.3以前ではtrue)

この例の$x1,$y1は整数の上限値となるような数値文字列です。これは整数としてピッタリ表現できるので、数値比較されます。これはPHP 5.4.3以前と同じ挙動です。


一方、$x2、$y2は整数の上限値を超えており、数値比較するとしたら浮動小数点数にキャストして比較することになります。実際、PHP 5.4.3までは浮動小数点数として数値比較されていました。しかし、これらの整数は大きすぎるため、浮動小数点数にキャストすると精度が落ちてしまい比較演算が不正確になるおそれがあります。このような場合にPHP 5.4.4では文字列比較するようになったというわけです。


整数に対応する数値文字列の場合、浮動小数点数にキャストして精度が落ちると判断されるのは-2^63-1以下または2^63以上(整数の範囲外かどうか、64bit環境の場合)のとき、もしくは-2^53以下または2^53以上(浮動小数点数仮数部で表現できるかどうか、32bit環境の場合)のときになります。


仕様変更での考え漏れ

ところで、この仕様変更では浮動小数点数に対応する数値文字列について仕様上の考え漏れがあるように思います。つまり、浮動小数点数の場合には「文字列比較しないと精度が落ちる」とはどういうことか、自明ではありません。


PHPソースコードを確認してみたところ、64bit環境では10^19以上の浮動小数点数に対応する数値文字列同士を浮動小数点数にキャストして差が0だった場合は文字列比較するという実装になっていました。10^19というのは浮動小数点数にとって意味のある境界値では無いため、深い考えがあるようには思えません。パッチを当てた人はそんな境界値になっていることすら気づいていない可能性があると思います。


上記の内容は、ソースコードZend/zend_operators.cのzendi_smart_strcmp()とZend/zend_operators.hのis_numeric_string_ex()を追えば確認できます。


浮動小数点数に対応する数値文字列の比較について、実行例を示します。


<?php
$x3 = "9999999999999999999.0";
$y3 = "9999999999999999999.1";
var_dump($x3==$y3); // bool(true), 数値比較している(文字列比較していたらfalseのはず)

$x4 = "10000000000000000000.0";
$y4 = "10000000000000000000.00";
var_dump($x4==$y4); // bool(false), 文字列比較している(数値比較していたらtrueのはず, PHP 5.4.3以前ではtrue)

ちなみに、$x3、$y3、$x4、$y4をそれぞれ浮動小数点数にキャストすると全て等しい数になります。にもかかわらず、数値文字列同士の比較では10進表記の桁数によって挙動が変わるということです。


まとめ

  • PHPで==を使って数値文字列同士を比較する場合、原則として数値比較されます
  • PHP5.4.4以降では、比較する数値文字列同士が大きくて両者が近すぎる場合には文字列比較になりました
    • 整数であれば-2^63-1以下または2^63以上(64bit環境)の場合
    • 浮動小数点数であれば-10^19以下または10^19以上(64bit環境)の場合
  • 比較演算子も影響を受けるので、ソート処理などにも注意が必要です

今まで特に悲鳴も聞こえてきていませんし、かなり大きい数にしか影響しないため、この変更による実害は無いだろうと考えています。とはいえ、こんなキモい修正について2年間誰も騒いでこなかったのはどうなのよ、とも思いますね。皆もう少し本体のソースコードを読みましょうよ。そう言う僕も2年間読んでなかったわけですけどね…。


参考リンク

norinori 2014/08/10 14:14 騒ぐ理由が分かりませんね。

hnwhnw 2014/08/10 14:49 特に分かっていただく必要は無いのですが、もし僕がこのコードを業務でコードレビューしたとすれば、何かしらコメントすると思います。普段のお仕事の内容が違うのかもしれませんね。

eighteight 2014/08/10 15:55 ソースコード読んで細かい事にネチネチ言ってるほうがよっぽどキモいと思う
チミが問題だと思うならバグレポなりなんなりして本家に反映させたほうがよっぽど有意義だよ

DSHDSH 2014/08/10 15:57 そもそも精度が怪しい数値をイコールで比較すること自体が間違ってるし、
バージョン上げるためにいちいちソース確認しなきゃなんないような
タイトな設計ならPHPなんて使わずにフルスクラッチで書くってば。

てゆっか、bccomp()でいいんでない?

hnwhnw 2014/08/10 17:33 eightさん:
ソフトウェア技術者にとってソースコードこそ最強の1次情報源だと思いますが、ソースコード読む奴キモいとか言う人が技術者を名乗ってるとしたら楽しい業界ですね。

DSHさん:
確かにサンプルコードのようなコードを書くことはまずありえないでしょう。しかし、==同様の比較はin_array関数やsort関数でも行われるので、同様の処理を書いてしまう可能性はゼロではないはずです。PHPプログラムを読み書きする仕事であれば、==の挙動を把握しておくことは重要だと思います。

また、OSSを採用する上でいざとなったらソースを確認する気概は重要だと僕は思います。そのコストが非現実的だと思うならプロプライエタリ製品を使うべきでしょう。たとえPHPを使わなかったとしても、コンパイラにもOSにもバグはありえます。

eighteight 2014/08/10 18:50 ソース読むことがキモいなんてただの一言も言ってない。
粗探ししてはdisるだけで俺phpソース読んでてかっこいいだろ、ってのが言いたいことなのがキモい。
ソースコード読めるのに日本語が読めないとしたら楽しい世界ですね

hnwhnw 2014/08/10 18:53 eightさん:
一応断っておくと、僕は日本人の中ではかなりPHPのバグレポ書いてる方ですよ。30件くらいですけど。パッチも採用されてる。で、あなたは?

DSHDSH 2014/08/10 23:47 まー、そこらへんは概ね同意。
部下がそんなコード書いてたら小一時間問い詰めるとこなんだけど、
ソフトウェア資産(笑)が地雷抱えてることは多々あるんで。

hnwhnw 2014/08/11 10:40 本体のソースコード読んだりバグレポ書いたりパッチ送ったりだけがcontributionじゃないし、そうしないと使えないなんてことも無いはずですが、本体のソースコードをもっと皆がカジュアルに読んだ方が各個人にとっても世界にとってもプラスになるんじゃないかなーと僕は思ってます。念のため補足でした。

inouetakuyainouetakuya 2015/03/30 18:13 == の挙動を調べていて、この記事を読みました。ありがとうございます、大変役に立ちました!!(^_^)

現実のコードにはまだまだ == が使われている箇所がいくつもあるので。

> ==同様の比較はin_array関数やsort関数でも行われるので、同様の処理を書いてしまう可能性はゼロではないはずです。PHPプログラムを読み書きする仕事であれば、==の挙動を把握しておくことは重要だと思います。

完全同意です。

 | 
ページビュー
1813185