노트

부동소수점 (IEEE 754)

Floating-Point Arithmetic (IEEE 754)

CS#python · 연결된 개념 4개

쉽게 말하면

부동소수점은 실수를 정해진 칸 수에 담는 방식이라, 1/3을 0.333…으로 적다가 칸이 모자라 자르는 일이 생겨요. 2진수에서는 0.1도 끝없이 이어져서 0.1 + 0.2가 정확히 0.3이 안 돼요.

비유가 깨지는 곳 언어 버그가 아니라 표현 방식의 성질이라 어디서나 똑같이 생겨요. 그래서 동등 비교 대신 허용 오차로 비교하고, 돈은 Decimal이나 원·센트 같은 정수로 다뤄요.

부동소수점(floating-point)은 실수를 부호(sign)·지수(exponent)·가수(mantissa, significand)로 나눠 정해진 비트 수에 담는 표현 방식이다. 대부분의 언어가 따르는 IEEE 754(IEEE Standard for Floating-Point Arithmetic) 배정밀도(double precision, 64비트)는 유효숫자가 십진수로 15~17자리 정도라, 그보다 정밀한 값이나 2진수로 끝나지 않는 소수는 근삿값으로 저장된다.

0.1 + 0.2            # 0.30000000000000004
0.1 + 0.2 == 0.3     # False

구조 (64비트)

부호지수가수
1비트11비트52비트
  • 값 = (−1)^부호 × 1.가수 × 2^(지수 − 1023)
  • 지수에 1023을 더해 저장하는 바이어스(exponent bias) 덕분에 지수를 부호 없는 수로 비교할 수 있다
  • 0.1은 2진수로 무한히 반복되는 수라(십진수의 1/3처럼) 52비트에서 잘린다. 언어의 문제가 아니라 표현 방식의 성질이다

비교와 계산

  • 동등 비교 대신 허용 오차로 비교한다: math.isclose(a, b, rel_tol=1e-9)
  • 큰 수와 작은 수를 더하면 작은 수가 사라질 수 있다. 많은 값을 더할 때는 math.fsum이 오차를 줄인다
  • 돈처럼 십진수가 정확해야 하는 값은 decimal.Decimal(문자열로 만든다: Decimal("0.1"))이나, 가장 작은 단위의 정수(원·센트)로 다룬다
  • 분수 그대로 계산해야 하면 fractions.Fraction
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")   # True

정수는 다르다

파이썬의 int는 메모리가 허용하는 한 크기 제한이 없다(임의 정밀도, arbitrary precision). 자바스크립트는 모든 숫자가 64비트 부동소수점이라 2^53을 넘는 정수가 정확하지 않아 BigInt가 따로 있다. 큰 ID를 JSON으로 주고받을 때 문자열로 보내는 이유다. 진법 변환은 bin()·oct()·hex()로, 긴 숫자는 1_000_000처럼 밑줄로 끊어 쓴다.

비교 코드에 매직 넘버처럼 박힌 허용 오차는 이름 붙인 상수로 뺀다(매직 넘버와 설명 상수). 숫자를 다루는 알고리즘의 비용은 빅오 표기법를 본다.

출처: Python 튜토리얼: Floating-Point Arithmetic · Representation Error

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 얕은 복사와 깊은 복사

    얕은 복사(shallow copy)는 바깥 컨테이너만 새로 만들고 안에 든 객체는 원본과 같은 객체를 가리킨다. 깊은 복사(deep copy)는 안쪽 객체까지 재귀적으로 새로 만든다. 중첩된 리스트·딕셔너리를 복사한 뒤 원본이 같이 바뀌는 버그의 원인이 대부분 이 차이다.

  • 파이썬 기본 컬렉션

    파이썬의 기본 컬렉션은 리스트·튜플·딕셔너리·셋 네 가지다. 가장 중요한 구분은 변경 가능 여부(mutable·immutable)와 순서·중복을 어떻게 다루는가다.

  • 재귀

    함수가 자기 자신을 다시 호출해 문제를 푸는 방식. 같은 함수를 점점 작은 입력으로 부르다가 더 나눌 필요가 없는 지점(기저 조건, Base Case)에서 멈춘다.

  • 분할 정복

    문제를 같은 모양의 더 작은 문제로 나누고(분할, Divide), 각각을 풀고(정복, Conquer), 그 결과를 합쳐(결합, Combine) 원래 문제를 푸는 방식. 대개 재귀로 표현하고, 나눌 때마다 크기가 절반으로 줄면 log n 단계 만에 바닥에 닿는다.

  • 인지 부하

    작업 기억이 한 번에 처리해야 하는 양. 문제 자체의 복잡함과 표현 방식에서 오는 복잡함으로 나뉜다.

보기 옵션