0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

浮動小数点数の仕組みを丁寧に解説 — なぜ `0.1 + 0.2` は `0.3` にならないのか

0
Posted at

この記事で伝えること

  • console.log(0.1 + 0.2)0.30000000000000004 になる理由が説明できるようになります
  • コンピュータが小数を「2進数」でどう表現しているかがわかります
  • IEEE 754という浮動小数点数の標準規格の考え方を理解できます
  • 実務で金額計算に浮動小数点数を使ってはいけない理由と、代わりにどうすればいいかがわかります

プログラミングを始めたばかりの頃、多くの人が一度はこの現象に驚きます。私自身も初めて見たときは「バグでは?」と本気で疑いました。今回はその正体を、2進数の仕組みから丁寧に解き明かしていきます。

基礎知識の解説

そもそも「2進数で小数を表す」とはどういうことか

私たちが普段使う10進数では、小数は次のように「10のマイナス乗」の組み合わせで表現されています。

0.25 = 2 × 10^-1 + 5 × 10^-2

コンピュータの世界では、これと同じことを2進数(0と1)でやります。つまり「2のマイナス乗」の組み合わせです。

0.5   = 1 × 2^-1
0.25  = 1 × 2^-2
0.125 = 1 × 2^-3

ここで重要なポイントがあります。10進数の 0.1 は、2進数では綺麗に表現できないのです。

>>> (0.1).hex()
'0x1.999999999999ap-4'

0.1 を2進数で書こうとすると、0.0001100110011001100110011... のように 1100 が無限に繰り返す循環小数 になってしまいます。ちょうど10進数で 1/3 を書こうとすると 0.3333... と無限に続くのと同じ現象です。

有限のビット数に無限小数を詰め込む

コンピュータのメモリは有限なので、この無限に続く小数をどこかで打ち切らなければなりません。これが丸め誤差の正体です。

一般的な言語(Python、JavaScript、Javaなど)では、小数は IEEE 754 という規格に基づく「倍精度浮動小数点数(double)」として扱われます。これは64ビットを次のように使い分ける方式です。

  • 符号部: 正負を表す(0=正、1=負)
  • 指数部: 小数点の位置(何乗するか)を表す
  • 仮数部: 実際の数値(有効桁)を表す

イメージとしては「理科の授業で習った指数表記」に近いです。

1.100110011...₂ × 2^-4

仮数部が52ビットしかないため、0.1 のような循環小数は途中で切り捨てられ、本来の値よりわずかに大きい、または小さい近似値として記憶されます。

なぜ 0.1 + 0.20.3 にならないのか

0.10.2 も、それぞれ実際には「真の値にほんの少しだけ誤差を含んだ近似値」としてメモリに格納されています。

0.1 の実際の値 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 の実際の値 ≈ 0.200000000000000011102230246251565404236316680908203125

この2つを足すと、誤差同士が積み重なり、0.3 にぴったり一致しない値になります。

>>> 0.1 + 0.2
0.30000000000000004

これは言語のバグではなく、IEEE 754を採用しているほぼ全ての言語で起きる共通の仕様です。

// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
# Python
print(0.1 + 0.2)  # 0.30000000000000004

具体例・実際の動作

誤差が問題になる典型例:金額の合計

浮動小数点数の誤差が一番怖いのは「お金の計算」です。

total = 0.0
for _ in range(10):
    total += 0.1

print(total)          # 0.9999999999999999
print(total == 1.0)   # False

10回 0.1 を足しただけで、期待する 1.0 とは一致しなくなります。ECサイトの合計金額計算などでこれが起きると、if total == 1000: のような比較が意図通りに動かず、深刻なバグにつながります。

対処法1: 誤差を許容した比較をする

import math

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

== で厳密比較せず、「十分近ければ等しいとみなす」比較関数を使います。

対処法2: 整数で計算する(金額なら「円」ではなく「銭」単位など)

# 100円と200円を足す例(円ではなく「銭」= 1/100円単位の整数で扱う)
price_a = 10050  # 100.50円
price_b = 20025  # 200.25円
total = price_a + price_b
print(total / 100)  # 300.75

整数同士の加減算には誤差が生じないため、金額はできるだけ「最小単位の整数」で扱うのが定石です。

対処法3: 10進数を正確に扱う専用の型を使う

Pythonには decimal モジュールという、10進数を正確に扱うための標準ライブラリがあります。

from decimal import Decimal

a = Decimal("0.1") + Decimal("0.2")
print(a)  # 0.3

内部で2進数変換をせず10進数のまま計算するため、0.1 + 0.2 が期待通り 0.3 になります。ただし通常の浮動小数点数より処理が重いため、パフォーマンスが必要な数値計算には向きません。

筆者の考え・所感

個人的には、浮動小数点数の誤差は「コンピュータが嘘をついている」のではなく「有限のリソースで無限を近似する」という、コンピュータサイエンスの本質が凝縮された現象だと思っています。無限に続く情報を有限のビット数で表現しようとすれば、どこかで妥協が必要になる。これはメモリの制約から生まれる、いわば必然の副作用です。

実務では、これまで何度か「なぜかテストの数値比較がたまに失敗する」という相談を受けたことがあります。原因を辿ると大抵は浮動小数点数の誤差か、== による厳密比較でした。この仕組みを知っているかどうかで、デバッグにかかる時間は大きく変わってきます。

また面白いのは、この問題が「JavaScriptのバグ」としてネタにされがちなことです。実際にはJavaScriptに限らず、Python・Java・C・Go・Rustなど、IEEE 754を採用するほぼ全ての言語で同じ現象が起きます。特定の言語を叩く前に、まず「これは言語仕様ではなく数値表現の仕様である」と理解しておくと、技術的な議論がより建設的になると感じています。

まとめ

  • コンピュータは小数を2進数(2のマイナス乗の組み合わせ)で表現しており、0.1 のような値は循環小数になり正確に表せない
  • IEEE 754規格では符号部・指数部・仮数部という有限のビット数に値を近似して格納するため、丸め誤差が発生する
  • 金額計算など誤差が許されない場面では、整数化や Decimal 型など「誤差を生まない」手段を選ぶことが重要
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?