paiza開発日誌

IT/Webエンジニア向け総合求人・学習サービス「paiza」(https://paiza.jp ギノ株式会社)の開発者が開発の事、プログラミングネタ、ITエンジニアの転職などについて書いています。

Python3で巨大な浮動小数計算の結果が変だったので理由を調べてみた

f:id:paiza:20170801131214j:plain
Photo by Jacob Munk-Stander
秋山です。

タイトルのとおり、Python3で巨大な浮動小数計算をした時の計算結果についての話です。

例えば、 11 × 10^{23} ÷ 10 という計算式があったとしましょう。

普通に人力で単純に計算しようと思ったら、10の23乗を計算して、それから11を掛けて…という手順になるかと思いますが、10の23乗の時点で 100000000000000000000000 という大きな数になってしまい(ちなみに千垓です。垓は万・億・兆・京の次になります)非常にわかりづらいですね。

これぐらいの桁数の数になってくると、プログラミングでもいわゆる32bitの整数型では表現することができません。64bit整数でも足りないので、128bit整数でやっと表現できるようになります。

私は普段paizaスキルチェック問題の制作を担当していて、自分でもいろいろな問題を解いたりコードを書いたりしているのですが(言語は主にPythonを使っています)、ある時、こうした長大な数値を割り算することになって、以下のようなコードを書いて実行結果を見てみました。

……計算結果がおかしくなってる?

演算子「 // 」を使って書き直したところ

正しい結果が出ましたね。

この違いは何なんだろう?と思ったので、今回はPython3で巨大な浮動小数計算をしたときの挙動について、いろいろ調べてみました。

■Python3で巨大な浮動小数計算したら結果がおかしくなったので調べてみた

まずは公式ドキュメントを見てみましょう。(※リンクは翻訳版です)

4. 組み込み型 — Python 3.6.1 ドキュメント

↑リンク先を見ると、整数の精度に関しては制限がない…という旨の記述があります。

「//」演算子を使用した場合は、一度も float になることがないはずなので、正しい計算結果が出るのですね。


ここで、float の使用をさける「//」の演算子の挙動が気になってきました。(floatを使ったときに何が起きたかは後述します)

10.3. operator — 関数形式の標準演算子 — Python 3.6.1 ドキュメント

うーん、ドキュメント上ではあまり詳しく書かれていないようなので…ソースをたどって、実際にどう実装されているのか見にいってみましょう。

CPythonのGitHubリポジトリからたどり着いたのが以下のソースです。

github.com

整数同士の割り算がこちらで行われていますね。


ここから、「/」演算子を使用した場合の、間違っている計算結果における float 誤差の正体を探ってみます。

4. 組み込み型 — Python 3.6.1 ドキュメント

↑このドキュメントにしたがって、 hex() というメソッドで浮動小数を16進数表現にしてみました。

「 // 」で割ったときの 110000000000000000000000
「 / 」で割ったときの 110000000000000004194304

どちらの数字も16進数では 0x1.74b1ca8ab05a9p+76 となりました。


ここからは、倍精度浮動小数の誤差についての話になります。

倍精度浮動小数点数 - Wikipedia

2^{52}=4,503,599,627,370,496 と 2^{53}=9,007,199,254,740,992 の間で表現できる数値は正確に整数に対応している。2^{53}から2^{54}までの範囲では、常にその2倍となるので、偶数しか表現できない。

という表記があります。

仮数部が倍精度だと52bitあり、2の53乗から2の54乗の範囲では偶数でしか表現できない…ということですね。2の54乗から2の55乗の範囲では、2の2乗の倍数のみが表現可能になります。

そうなると、仮数部が2の76乗の 110000000000000000000000 を扱いたい場合も、 52-76 = 24 で、2の24乗の倍数しか表現できなくなってしまいます。

109999999999999995805696 0x1.74b1ca8ab05a8p+76 〜 この間はすべて 0x1.74b1ca8ab05a9p+76 〜 110000000000000012582912 0x1.74b1ca8ab05aap+76
その区間の幅は、2の24乗 で 16777216

というわけで、計算結果がおかしくなった原因は float で表現不可能な数字を float として扱おうとしたから…でした。

ちなみに、他の解決手段としては Decimal を使うという手もあります。10進数としての精度が指定できて、デフォルトでは28桁までが保証されています。使ってみるとこんな感じですね。

■まとめ

プログラミングをしていると、今回のような浮動小数計算に限らず「なぜか実行結果がおかしい」とか「思った通りに動作しない」といったことがあるかと思います。

そんなとき、「こういう仕様だから」で終わるのも悪くはないのですが、ドキュメントをあたってみたり、実装されているソースを見にいったりしてみると、「なぜそうなるのか」がわかって、その言語に対する理解も深まるのではないかと思います。


浮動小数計算どころかプログラミング自体が初心者なので、まずはPythonを使えるようになりたい!」という方は、プログラミングが動画で学べる「paizaラーニングPython入門編から始めてみると、無理なく基礎を習得できると思います。
paiza.jp

途中でブログパーツとして使ったオンライン実行環境サービス「paiza.IO (パイザ・アイオー)」はこちら




paizaは、技術を追い続けることが仕事につながり、スキルのある人がきちんと評価される場を作ることで、日本のITエンジニアの地位向上を目指したいと考えています。

自分のスキルを磨いていきたいという方におすすめなのが「paizaラーニング」。オンラインでプログラミングしながらスキルアップできる入門学習コンテンツです。初心者でも楽しくプログラミングの基本を学ぶことができます。
paiza.jp
そして、paizaでは、Webサービス開発企業などで求められるコーディング力や、テストケースを想定する力などが問われるプログラミングスキルチェック問題も提供しています。
paiza.jp
スキルチェックに挑戦した人は、その結果によってS・A・B・C・D・Eの6段階のランクを取得できます。必要なスキルランクを取得すれば、書類選考なしで企業の求人に応募することも可能です。「自分のプログラミングスキルを客観的に知りたい」「スキルを使って転職したい」という方は、ぜひチャレンジしてみてください。

ITプログラマ・エンジニア向け転職・就活・学習サービスのpaiza