公開

0.1+0.2が0.3にならない理由|2の補数・浮動小数点・丸め誤差

文:野澤 嘉孝 (ソフトウェアエンジニア/プログラミングスクール Sandbox 代表)

0.1+0.2が0.3にならない理由|2の補数・浮動小数点・丸め誤差

この記事の概要

0.1と0.2を足すだけなのに、パソコンが「0.30000000000000004」と答えるのはなぜでしょう? 計算を間違えたのでしょうか? 原因は、0.1や0.2を、一般的な2進浮動小数点数ではぴったり保存できないことです。 数を保存する仕組みを、部室の小さな事件から見ていきましょう。

0.1と0.2を足すだけなのに、パソコンが「0.30000000000000004」と答えるのはなぜでしょう? 計算を間違えたのでしょうか?

原因は、0.1や0.2を、一般的な2進浮動小数点数ではぴったり保存できないことです。

数を保存する仕組みを、部室の小さな事件から見ていきましょう。

先に結論:足す前から、少し違う値を保存している

  1. 0.1や0.2は、2進法では無限に続く小数になる。 10進法の1/3=0.333…に似ている。
  2. 保存できる桁数には限りがあるため、近い値に丸める。 元の値とのずれが「丸め誤差」。
  3. 近似値どうしを足し、結果も丸める。 一般的なPythonのfloatでは、0.1+0.2の表示が0.30000000000000004になる。

数学の0.1+0.2=0.3は正しいままです。問題は、数学の値と、コンピュータに保存できる値の違いにあります。

部費の計算に、見覚えのない「4」が現れた

放課後の部室。ユウキが初めて自分で書いた部費の計算プログラムで、試しに0.1と0.2を足してみると……。

print(0.1 + 0.2)
# 0.30000000000000004

ユウキ「……なんか、多い」

テッパン先輩「多いね」

ユウキ「バグですか?」

Sandy「ちょお待ちぃ! パソコンが適当に計算したわけやないで。数をどう保存しとるかが関係してんねん」

0.1+0.2=0.3という数学の式は正しいままです。調べるべきなのは、プログラムが保存した値と、計算のルールのほうです。

0.1と0.2を近似して保存し、和も丸めて表示する流れ

ズレは入力の保存時から始まる。

※上の結果は、一般的なPython環境のfloatによる例です。使う数値型・精度・表示方法によって見える結果は変わります。

小数の誤差も、整数の桁あふれも、「数をどんな形式で保存するか」に関係します。まず整数の箱を確認し、そのあと小数の仕組みへ進みましょう。2の補数は負の整数の表し方であり、0.1+0.2のずれの直接の原因ではありません。

整数にも箱の大きさがある:符号なし・符号付き

コンピュータは数をビット列で表します。まずは8ビットという、8個の0か1を並べる箱を考えましょう。組み合わせは2の8乗、256通りです。

表し方

8ビットで表せる範囲

ポイント

符号なし整数

0〜255

0以上の整数に256通りを使う

符号付き整数(2の補数)

−128〜127

負の数にも組み合わせを割り当てる

同じビット列でも、読み方が変われば意味が変わります。たとえば11111111は、符号なしなら255、8ビットの2の補数なら−1です。

「整数なら何でも入る」わけではありません。ただし、整数の桁数が常に8ビットに固定されているわけでもありません。 ここでは仕組みを学ぶために8ビットに限定しています。Pythonの通常の整数intは、メモリの許す範囲で大きな値を扱えます。

2の補数は、ビットを反転して1を足す

2の補数は、決められたビット数で負の整数を表す方法です。8ビットで5から−5を作るなら、次の2手です。

  1. 00000101の0と1をすべて反転する → 11111010
  2. 1を足す → 11111011
8ビットの5を反転し1を足して−5のビット列を作る手順

2の補数では反転して1を足す。

Sandy「ひっくり返して、1を足す。手順はこれだけや」

ユウキ「じゃあ左端だけ1にして、10000101でも……」

Sandy「それやと別の数や! 2の補数では、8桁全体で読むねん」

8ビットの2の補数で10000101は−123です。左端を符号、その右側をそのまま絶対値、と読む方式とは違います。

2の補数を使うと、3−5を3+(−5)として扱えます。ビット列でも00000011+11111011=11111110となり、結果は−2。加算の仕組みを減算にも利用でき、0の表現も1つで済みます。

桁あふれ:127に1を足したら?

8ビットの2の補数で127は01111111です。1を足し、8ビットの結果だけを残すと10000000になり、−128と解釈されます。

これは**表現範囲を超える「桁あふれ(オーバーフロー)」**の例です。数学の答え128を、この箱では表せません。

ただし「どの言語でも127+1=−128」ではありません。桁数を増やす、エラーにするなど、処理は言語や型によって変わります。たとえばPythonの通常のintなら128になります。

小数はどう入れる? 固定小数点と浮動小数点

小数の表現には、小数点の位置を固定する方法と、指数を使って数の尺度を変える方法があります。

表現

考え方

特徴

固定小数点

小数点の位置・尺度を固定する

決めた範囲と刻み幅で扱う

浮動小数点

有効な数字と指数を組み合わせる

幅広い大きさを扱えるが、精度は有限

同じビット数の固定小数点では、細かい値を表せるようにすると、大きな値を表す余裕が減ります。浮動小数点は指数を変えて幅広い大きさに対応しますが、すべての値を正確に保存できるわけではありません。

符号部・指数部・仮数部の役割

浮動小数点は、理科で使う「1.2×10の3乗=1200」のような指数表記で考えると分かりやすくなります。これは10進法のたとえで、ここで扱うコンピュータの形式は2進法です。

  • 符号部:正か負かを表す。
  • 指数部:数の大きさの尺度を表す。
  • 仮数部:有効な数字を表す。
浮動小数点の符号部・指数部・仮数部とビット配分

浮動小数点は3つの役割に分かれる。

IEEE 754の代表的な形式では、単精度32ビットは符号1・指数8・小数部分23ビット、倍精度64ビットは符号1・指数11・小数部分52ビットです。

通常の正規化数では、先頭が1と分かっているので、その1を省略して記録します。そのため、有効な精度はそれぞれ24ビット・53ビットです。指数部も実際の指数をそのまま記録するわけではありません。まずは、ビット数の暗記より「符号・大きさの尺度・有効な数字」の役割を押さえましょう。

正規化とは、同じ値の表し方を一定のルールにそろえることです。ここで扱う2進形式の通常の正規化数では、絶対値を 1.xxx… × 2の整数乗 の形にします。たとえば、2進数の 101.1 は 1.011 × 2の2乗 と表せます。0や非正規化数などは別扱いです。教材や問題に別の表記ルールがある場合は、その指定に従いましょう。

なぜ0.1は2進法で終わらないのか

10進法で1/3を書くと0.333…と続きます。同じように、1/10を2進法で書くと0.0001100110011…と続きます。10進法で短く書けることと、2進法で短く書けることは別なのです。

Sandy「0.1って、見た目は短いやろ。でも2進法に引っ越したら、荷物が終わらへんねん」

分数にして、約分した分母を見る

有理数が2進法で有限小数になる条件は、約分後の分母が2の累乗であることです。整数は分母1=2の0乗と考えられます。

10進数

約分した分数

2進法で有限桁になる?

0.5

1/2

なる:0.1

0.25

1/4

なる:0.01

0.375

3/8

なる:0.011

0.1

1/10

ならない:分母に5が残る

0.2

1/5

ならない:分母に5が残る

0.375と0.1の分母を比べて2進有限小数を見分ける図

約分後の分母が2の累乗かを調べる。

判定の前に約分するのが大事です。0.375を375/1000のまま見ると、分母に5があるように見えますが、約分すると3/8です。

なお、有限小数になることと、特定の浮動小数点形式に正確に収まることも別です。必要な有効桁数や指数が、その形式の制限内に収まる必要があります。

丸め誤差・情報落ち・桁落ちは、原因で区別する

無限に続く桁を有限の箱へ入れるには、近い値で代用する必要があります。その際の元の値との差が丸め誤差です。入力の保存時だけでなく、計算結果を保存する際にも丸めが起こり得ます。

丸め誤差・情報落ち・桁落ちを原因で整理した図

似た誤差の用語は、起きる原因で区別する。

用語

何が起きる?

丸め誤差

数を有限の桁数へ丸めた結果、元の値からずれる

情報落ち

大きさが著しく違う数の加減算で、小さい数の情報が結果に反映されないことがある

桁落ち

近い値の引き算などで上位桁が相殺され、有効な桁数が減る

打ち切り誤差

級数など、本来続く計算を有限の手順で打ち切るために生じる

たとえば一般的なPythonのfloatでは、1e16+1の結果は1e16と等しくなります。その大きさでは表せる値の間隔が広く、足した1が結果に残らないためです。数を大きくすれば、いつでも細かい差まで持てるわけではありません。

桁落ちは、引き算自体が必ず不正確になるという意味ではありません。入力に含まれていた誤差が、差に対して大きな割合を占める問題にも注意が必要です。

最初の謎の答え:足す前から近似値だった

0.1も0.2も、一般的な2進浮動小数点数では正確に保存できません。それぞれ近似した値を足し、結果も表現可能な値へ丸めます。その結果の表示が、冒頭の0.30000000000000004です。

なお、画面に 0.1 とだけ表示されても、内部の値が正確な0.1とは限りません。Pythonは通常、保存した値を読みやすい短い10進表記で表示します。保存された値と、画面に表示する桁数は別なのです。

ユウキ「計算が適当なんじゃなくて、保存した時点で少し違う値だったんですね」

Sandy「せや。機械の故障を疑う前に、箱のルールを見るんや」

学習例:2進法で有限小数になるのは?

この記事用のオリジナル学習例です。実際の共通テスト問題ではありません。

0.15・0.625・0.7のうち、2進法で有限小数になるものを選んでください。

答えは0.625。5/8なので、2進法では0.101です。1/2+1/8=0.625と確かめられます。0.15は3/20、0.7は7/10で、どちらも約分後の分母に5が残ります。

見た目の小数の桁数ではなく、約分後の分母で判断しましょう。

プログラムでは、比較と金額計算をどうする?

誤差を含む計算結果を「ほぼ等しいか」で比べたい場合は、許容する差を決めます。Pythonならmath.iscloseが使えます。

import math

print(0.1 + 0.2 == 0.3)           # False
print(math.isclose(0.1 + 0.2, 0.3)) # True

許容差は「どこまでのずれなら同じとみなすか」という基準です。たとえば測定値なら、その測定に必要な精度に合わせます。特に0付近の比較では、絶対許容差 abs_tol の指定を検討します。小数を等号で比べることが一律に禁止なのではなく、近似値を完全一致で比べてよいかを考えるのがポイントです。

金額が1円単位で完結するなら、整数の円で持つ方法があります。税率や割合の計算で1円未満が出る場合は、丸めるタイミングと方法も決めましょう。

10進数として計算したい場合は、Pythonの Decimal も選択肢です。

from decimal import Decimal

print(Decimal('0.1') + Decimal('0.2'))  # 0.3

文字列の '0.1' から作るのがポイントです。Decimal(0.1) では、すでにfloatに入った近似値を引き継ぎます。ただしDecimalでも精度は有限で、1÷3などには丸めが必要です。

表示を小数第1位に整えるだけでは、内部の値が数学的に正確になったことにはなりません。

まとめ:数の「箱」と「読み方」を確かめよう

  • 固定幅の整数には範囲がある。2の補数では、ビット列全体で負の数を表す。
  • 浮動小数点は、符号・指数・仮数で数を表す。広い範囲を扱えても、精度は有限。
  • 0.1は2進法で無限に続く。保存や演算で丸めが起こり、結果にずれが現れる。
  • 誤差を含む比較や金額計算は、必要な精度と丸めのルールを決める。

ユウキは部費の金額を整数の円で持つように書き直しました。今度は、目的に合った方法で計算できそうです。

数の表し方を学ぶと、「変な数字が出た」で止まらず、その理由を説明できるようになります。プログラミングスクールSandboxでは、こうした仕組みを実際のプログラムや作品制作につなげて学べます。

参考資料

あわせて読みたい記事

Sandboxのサポート内容 Sandboxの情報Ⅰ学習サポートを見る →
共通テスト情報Ⅰのまとめ 共通テスト「情報Ⅰ」対策シリーズの一覧を見る →
野澤 嘉孝

この記事を書いた人

野澤 嘉孝

ソフトウェアエンジニア/プログラミングスクール Sandbox 代表

ソフトウェアエンジニア。同志社大学理工学部を経て、京都大学大学院で核融合発電の基礎研究(プラズマ物理)に従事。高校生向け数学塾の講師、大学での物理実験の授業を通じて延べ500名以上の学生をサポート。現在は業界特化型SaaSの開発に携わりながら、小中高生向けプログラミングスクール Sandbox を運営。

プロフィールと執筆記事の一覧 →

「情報Ⅰ、うちの子はどう対策すればいい?」

情報Ⅰ対策について無料で相談する

志望校の配点を一緒に確認したい方は → 無料の学習相談へ

入会金無料・いつでも退会OK・退会後もいつでも再入会できます