0.1이 진짜 0.1이 아닌 이유(부동소수점(Floating Point))
1. 0.1 + 0.2 는 0.3이 아닌 0.30000000000000004다?
0.1 + 0.2 는 얼마일까?
이 질문을 본다면 당연히 0.3이라고 대답할 것이다. 그렇다면 컴퓨터에서도 0.1 + 0.2 = 0.3이라는 결과가 나올까?
public class App{
public static void main(String[] args){
System.out.println(0.1 + 0.2);
}
}
위의 코드를 실행하면 어떤 결과 값이 나올까? 당연히 0.3이라는 결과값을 기대하고 실행시킬것이다.
하지만 결과는 0.3이 아닌 0.30000000000000004가 나오게된다.
Java가 아닌 JavaScript, python 등으로 실행해도 0.3이 아닌 다른 값이 나오게 된다.
왜 이런 결과값이 나오게 될까??
사실 0.1 + 0.2 = 0.3이 아니고 0.30000000000000004였던 것일까???
그 이유는 바로 컴퓨터에서 0.1과 0.2는 사실 진짜 0.1과 0.2가 아니기 때문이다.
0.1가 0.2가 진짜 0.1과 0.2가 아니라니 이게 무슨소리일까??
2. 0.1이 진짜 0.1이 아닌 이유
컴퓨터는 2진법을 사용해서 문자와 숫자들을 표현하고 있다. 2진법으로는 모든 숫자들을 정확하게 표현하지 못한다.
예를들어 9.625란 수를 2진법으로 바꾸어 표현해보자. 1001.101로 표현할 수 있을것이다.
그렇다면 0.1을 2진법으로 표현하면 어떻게 될까?
0.0001100110011.......로 끊임없이 이어지는 무한소수가 된다. 이를 메모리에 다 저장할 수 없기 때문에 어쩔 수 없이 컴퓨터는 0.1의 근사치를 저장하게 되고 여기서부터 0.1은 더이상 0.1이 아닌 0.1에 가까운 수가 되어 오차가 발생하게 된다.
그래서 0.1 + 0.2가 0.3이 아닌 0.30000000000000004가 나오는 것이다.
그렇다면 컴퓨터가 실수의 근사치를 저장하는 방법에는 뭐가 있을까?
3. 고정소수점(Fixed Point) vs 부동소수점(Floating Point)
3.1. 고정소수점(Fixed Point) 방식
고정소수점 방식은 정수를 표현하는 비트 수와 소수를 표현하는 비트 수를 미리 정해 놓고 해당 비트 만큼만 사용해서 숫자를 표현하는 방식이다.
예를들어 실수 표현에 4byte(32bit)를 사용하고 그 중 부호 1bit, 정수 16bit, 소수 15bit를 사용하도록 약속해 놓은 시스템에 있다고 가정하고 9.625를 고정소수점방식으로 표현해보자.
우선, 9.625를 2진법으로 바꾸면 1001.101이 되고 이를 부호 1bit, 정수 16bit, 소수 15bit에 맞춰 표현하면
0(부호) 0000000000001001(정수) 101000000000000(소수) 이 된다.
이러한 고정소수점 방식은 구현하기 편리하지만 사용하는 비트 수 대비 표현 가능한 수의 범위 또는 정밀도가 낮다.
이게 무슨 말이냐 만약 큰 수를 표현해야해서 정수를 표현하는 bit를 늘리면 큰 수를 표현할 수 있지만 정밀한 수를 표현하긴 힘들다. 그래서 정밀한 수 표현을 위해서 소수를 표현하는 bit를 늘리면 정수 부분의 bit가 줄어들어 큰 수를 표현하지 못하게 된다.
이러한 단점 때문에 고정소수점 방식은 거의 사용되지 않는다. 그렇다면 어떤 방식을 사용하는 걸까?
3.2 부동소수점(Floating Point) 방식
부동소수점 방식은 2진수로 변환한 숫자를 1.xxxx... * 2^n 의 형식으로 정규화(Normalization)를 시킨다.
바꾸는 방법은 간단하다. 정수부에 1만 남기고 나머지를 다 소수점을 왼쪽으로(맨 처음숫자가 0인경우 오른쪽으로) 이동시키기고 이동시킨 수 만큼 n에 넣어서 표현해 주면 된다.
9.625를 2진수로 바꾼 1001.101를 예를 들어보면
1001.101에서 소수점 앞에 1만을 남기고 다 뒤로 넘겨준다.
그러면 1.001101 * 2^3이 된다.
이처럼 소수점의 위치를 고정시키지 않고 이동 시켜서 표현하기 때문에 부동소수점(Floating Point)라고 한다.
부동소수점을 표현하는 방식으로는 대부분 IEEE에서 표준으로 제안한 방식인 IEEE 754를 사용한다.
32bit인 float의 경우는 아래와 같은 방식으로 표현한다.
64bit double은 부호 1bit, 지수 11bit, 가수 52bit의 구조를 가지고 있다.
- 부호 비트 : 고정소수점 방식에서처럼 0이면 양수 1이면 음수이다.
- 가수 비트 : 정규화 시킨 소수점 부분을 앞에서 부터 써준 다음 남는 부분은 0으로 채워준다. (참고: 소수점 왼쪽은 정규화를 하면 무조건 1이기 때문에 신경쓰지 않고 표현도 안 하는데, 이 1을 hidden bit라고 부르기도 한다)
- 지수 비트 : 정규화 시킨 값에서 2^n 중에 n에 해당하는 부분이다. 그러면 위에서 9.625를 정규화 시킨 1.001101 * 2^3에서 3을 2진수로 바꾼 11을 넣으면 되는걸까? 정답은 아니다!
IEEE 표준에 따르면 저 부분에는 지수를 그대로 넣어주는게 아니라, bias라고 하는 지정된 숫자를 더한 다음 넣어야 한다.
IEEE 표준에서 32bit를 쓰는 경우 bias는 127이라고 규정하고 있다. 따라서 3 + 127 = 130를 2진수로 바꾼 10000011이 들어가야한다.
이제 9.625를 부동소수점 방식으로 이렇게 표현할 수 있다.
- 부호 비트(1 bit) : 0 (양수)
- 지수 비트(8 bit) : 10000011 (127 + 3 = 130)
- 가수 비트(23 bit) : 00110100000000000000000
그렇다면 bias는 왜 사용해야될까??
그 이유는 지수가 음수가 될 수도 있어서 그렇다. 위에서 부동소수점 표현을 위한 정규화를 하면서 우리는 정수 부분을 1로 만들어야했다.
예를들어 0.0101이라는 실수가 있다고 가정하자. 이를 정규화하면 정수 부분을 1로 만들기 위해 소수점을 오른쪽으로 2번 이동시켜
1.01 * 2^(-2) 로 변환 시킬 것이다. 이 상태에서 만약 bias가 없다면 우리는 -2를 어떻게 저장해야할까?
부호 비트는 지수가 아닌 전체 값이 양수인지 음수인지 판단하는 것이다.
그렇다고 지수가 양수인지 음수인지 판별하는 값을 새로 만들기에는 너무 복잡하다.
그렇기 때문에 bias 라는 값을 사용해서 지수가 음수가 되어도 값을 표현할 수 있게 해주는 것이다.
(32bit 기준 bias와 지수를 더한값이 1 ~ 127까지는 지수가 음수 128 ~ 255까지는 지수가 양수라고 생각하면 된다.)
이처럼 부동소수점 방식을 사용하면 고정소수점 방식보다 훨씬 더 많은 범위까지 표현할 수 있다.
하지만 부동소수점 방식에 의한 실수의 표현은 항상 오차가 존재한다는 단점을 가지고 있다.
그렇다면 돈계산과 같이 정확환 실수 계산이 필요한 경우에는 어떻게 해야할까?
방법은 여러가지가 있다.
1. 새로운 단위를 사용해서 소수점 부분을 없애준다.(예 : 1.1달러 -> 110센트 즉, float나 double을 사용하는 것이 아닌 int 나 long을 사용할 수 있도록 바꿔준다.)
2. 라이브러리를 사용한다( 예 : Java는 BigDecimal 사용)
결론 :
float와 double과 같은 실수 계산에 오차가 있음을 항상 명심하자!
참고 자료
'Java' 카테고리의 다른 글
해시맵(HashMap)이란? (0) | 2023.01.11 |
---|---|
클래스 변수(Class variable) vs 인스턴스 변수(Instance variable) vs 지역 변수(local variable) (0) | 2022.12.31 |
Java는 포인터(Pointer)가 없는데 왜 NullPointerException이 발생할까? (0) | 2022.12.27 |
JAVA 자료형 (0) | 2022.02.22 |
Java의 언어적 특징 (0) | 2022.02.21 |