contact

부동소수점과 부동소수점 연산 알아보기

항상 들어왔던 그 단어 부동소수점, 확실하게 짚고 넘어가기

부동소수점이란?

부동소수점, Floating Point는 컴퓨터에서 실수를 표현하는 방식 중 하나이다. 고정소수점 Fixed Point도 존재하지만, 현대 컴퓨터 시스템에서는 잘 쓰이지 않는 편이고, 임베디드 등 특정 상황에 더 효율적인 메모리 활용을 위해 쓰이고 있다.

% 10진법 예시(실제 컴퓨터 시스템에서는 2진법 사용)

45.67 -> 4.567 x 10^2
0.00987 -> 9.87 x 10^(-3)

위와 같이 유효숫자(가수) * 10의 거듭제곱(지수)의 형태로 표현하는 것이 부동소수점 방식이다. 실생활에서는 10진법을 쓰므로 위의 예시처럼 표현하지만, 컴퓨터에서는 2의 거듭제곱을 사용한다.

컴퓨터에서의 부동소수점 사용법 (IEEE 754)

컴퓨터에서는 부동소수점 사용법이 IEEE 754 표준으로 제정되어있다.

  1. 부호(Sign) - 음수, 양수 여부
  2. 지수(Exponent) - 지수(2의 승수)
  3. 가수(Mantissa) - 유효숫자

위의 세 부분으로 나뉘어있는데, 실제 컴퓨터 시스템의 관점으로 직접 한번 뜯어보자.

32비트와 64비트, 시스템에 따라 다른 정밀도

분류 32비트 64비트
부호 1비트 1비트
지수 8비트 11비트
가수 23비트 52비트

64비트 시스템이 도입되면서 수의 단위가 늘어났다. 덕분에 32비트 시스템보다 더 정밀하고, 더 넓은 범위의 부동소수점 수를 다룰 수 있다.

부호를 제외했을 때, 32비트 수는 범위가 대략 10^(-38)부터 10^38정도이지만, 64비트 수는 10^(-308)부터 10^308까지의 범위를 표현 가능하다. 10^(-308)이면 0.0000000...001인데, 0이 소수점 아래에 308개 있다고 보면 되겠다. 굉장히 정밀해졌다.

다만 비트 수가 늘어나며 다루는 수의 기준 단위가 커진것이지, 연산 방식은 32비트나 64비트나 동일하다.

구조 뜯어보기

32비트 컴퓨터 시스템에서 0100001010100000000000000000000이라는 32자리 이진수가 있을 때를 생각해보면 된다. 생각보다 쉽다. 부호 1비트/지수부 8비트/가수부 23비트로 나눈 뒤 연산 후 다 곱하면 된다. 단 지수부에서 bias의 존재를 잊으면 안된다.

부호 지수부 가수부
이진수 0 10000010 10100000000000000000000
계산 방법 0은 양수 10진수 변환값 130에서 bias 127을 빼기 1.x에서 x자리에 가수부를 앞에서부터 붙여 이진수 1.101으로 만들면 됨
십진수 +1 3 1.625

위의 표를 참고해 계산하면, 0100001010100000000000000000000은 지수부는 2^3이고 가수부는 1.625인 양수이다. 따라서 다 곱하여 (+1) * (2^3) * (1.625)를 구하면 십진수 13이 나온다. 이렇게 십진수로 변환을 마쳤다.

위와 같은 방식으로 부동소수점 수를 계산할 수 있다. 부동소수점은 효율적으로 메모리공간을 사용한다는 장점이 있지만, 그로 인해 정밀도가 떨어지는 편인데, 바로 알아보자.

부동소수점 표현 방식의 부정확성

부동소수점 방식 사용시, 2진수로 변환될 때 정확하게 표현할 수 없는 실수가 존재한다. 예를 들자면, 0.1을 이진법으로 표현할 때 0.00011001100110011...와 같이 무한소수가 되는데, 왜 그런지 알아보자

십진수 0.1 값을 나타내보자

이진수 십진수
0.1 0.5
0.01 0.25
0.001 0.125
0.0001 0.0625
0.00001 0.03125
0.000001 0.015625
... ...

십진수 소수 0.1을 무식하게 하나씩 더하는 방식으로 구할 건데, 그러기 위해선 (정수와 다르게)점점 절반으로 쪼개지는 작은 수들을 더해줘야한다. 미리 말해주자면 1을 절반씩 쪼개 십진수 0.1은 나오지 않는다. 그러므로 더 작은 수들을 더해서 최대한 십진수 0.1과 비슷한 수를 구해야한다.

0.5, 0,25는 너무 커서 OUT, 0.125는 오차가 0.025정도로 그나마 비슷하다. 하지만 0.06250.03125를 더하면 0.09375가 되고, 더 작은 수들을 더하면 오차를 더 줄일 수 있을 것이다.

그래서 현재까지 이진수 0.000011 = 십진수 0.09375를 구한 상태이고, 오차 0.00625를 메워줘야한다. 그러려면 더 작은 수 중 알맞은 수를 구해 더해야한다.

index 이진수 십진수
... ...
1 0.000001 0.015625
2 0.0000001 0.0078125
3 0.00000001 0.00390625
4 0.000000001 0.001953125
5 0.0000000001 0.0009765625
... ...

지금은 십진수 0.1을 구한다기보다는, 십진수 0.00625를 구한다고 생각해보자. (저 오차값만 구하면 0.1을 구할 수 있다.)

범위가 엄청 세밀해졌으므로 여기선 index를 붙여 헷갈리지 않게 부르겠다.

2번 수 0.00781250.00625와 비슷한 값이다. 0.0015625만큼의 작은 오차만 난다.

하지만 3번 0.00390625와 4번 0.001953125를 더해 0.005859375를 구하면 오차가 더 작다. 거기에다 작은 수를 계속 더하면 오차가 메워질 것이다.

3번 수와 4번 수의 합을 이진수로 나타내면 0.000000011이므로, 앞 단계에서 구한 0.00011과 더하면 0.000110011이다.

이제 다시 작은 수를 더해 오차를 채워야한다. 그러나 이미 알고 있겠지만, 이런 과정을 아무리 반복해도 오차는 메워지지 않는다.

0.0001100110011

0.00011001100110011

0.000110011001100110011...

위와 같은 패턴으로 무한반복될 뿐이다. 바로 무한소수이다.

부동소수점 근삿값

위에서 본 것과 같이, 정확히 떨어지지 않는 소수는 무한소수의 근삿값으로 표현된다. 정확도는 사용하는 시스템의 비트 크기에 따라 결정된다. 당연하지만 비트 자릿 수가 더 많은 64비트가 더 정확하다.(물론 64비트에서도 32비트 부동소수점 float를 사용가능하다.)

십진수 32비트에서 실제 값 64비트에서 실제 값
0.1 0.100000001490116119384765625 0.1000000000000000055511151231257827021181583404541015625
0.2 0.20000000298023223876953125 0.200000000000000011102230246251565404236316680908203125

이러한 근삿값을 사용하기 때문에 아래 코드는 에러가 난다.

# Python

a = 0.1 + 0.2
print(a == 0.3)  # False
// JavaScript

const a = 0.1 + 0.2;
console.log(a === 0.3); // false

부동소수점 방식의 근삿값을 활용하기 때문에 나타나는 문제이다. Python이나 JavaScript만의 문제가 아니라, IEEE 754 표준을 사용하는 대부분의 프로그래밍 언어에서 나타나는 공통적인 현상이다.

정리

부동소수점은 메모리 공간를 효율적으로 활용하여 넓은 범위의 실수를 표현가능한 표준이다. 하지만 그러한 특성으로 인해 작은 수를 다룰 땐 정밀성 문제가 나타난다. 따라서 이러한 실수를 다룰 땐 오차 발생의 가능성을 염두해두어야한다. 금융 서비스에서 이러한 오차로 인해 실수가 나면 큰일날 것이다. (이를 해결하기 위해 Epsilon 비교 기법이라는 것이 있으니 참고할 수 있다.) 이제 부동소수점의 특성을 알았으니 실수를 더 정확하게 다루도록 하자.

← blog