돈 계산과 같은 높은 정확도를 요구하는 연산 과정에선 float와 double를 사용하지 말라고들 합니다.
아마도 float과 double이 정확도가 낮기 때문이겠죠?
실제로 if (floatVariable == 0)을 지양하라고 합니다. 왜그럴까요?
왜 float와 double은 정확도가 안좋을까요?
적당히 깊게 원리까지 알아보고, 대안을 찾아봅시다.
결론부터 말하면
float와 double은 부동 소수점으로 실수를 표현하기 때문에 정확도가 낮습니다. 또한 부동 소수점 표현 방식은 연산 과정에서 지수부를 일치시킨 후, 가수부 끼리 계산 후, 지수부를 다시 일치시키는 과정을 겪어야 하기 때문에 연산 속도가 느립니다. 이러한 이유로 소수점 계산 시, 이를 정수로 계산한 뒤 소수로 바꾸는 과정을 거치라고들 하죠.
근데 무슨 말인지 바로 이해하기 어려우시죠?
부동 소수점? 지수부? 가수부?
이제 알아봅시다.
실수 표현 방식
고정 소수점
정의: 고정 소수점은 소수점을 사용하여 고정된 자리수의 소수를 나타내는 것이다. 한정된 메모리에서 부동소수점 방식보다 좁은 범위의 수만 나타낼 수 있다. (출처: 위키백과)
[그림1]은 float를 고정 소수점으로 표현한 그림입니다. 4바이트=32비트 중 부호 비트를 제회한 31비트로 정수부와 소수부를 구분합니다. 정수부와 소수부 사이에 소수점이 고정되있다고도 볼 수 있죠. 정수부는 0 ~ 2^15 - 1, 소수부는 0 ~ 2^16 - 1까지 표현 가능합니다. 이론적으로 (+-)32768.65535까지 표현할 수 있죠. 특수한 경우를 제외하면 정수부 5자리와 실수부 5자리는 너무 적습니다. 따라서 대부분의 언어에서는 고정 소수점이 아닌 부동소수점 방식으로 실수를 저장하게 됩니다.
부동 소수점
정의: 컴퓨터에서 실수를 표시하는 방법으로, 소수점의 위치를 고정시키지 않으며 가수와 지수를 사용하여 실수를 표현한다. 가수는 유효숫자를 나타내며 지수는 소수점의 위치를 나타낸다. (출처: 네이버 지식백과)
부동 소수점은 표현하고자 하는 수를 (가수) X (밑수)^(지수)와 같은 형태로 표현한다. 밑수는 일반적으로 2나 10, 16을 사용한다. 컴퓨터 메모리는 비트로 이루어져있기에 밑수 2를 사용한다. 결국 컴퓨터는 (가수) X 2^(지수) 형태로 부동 소수점을 활용하여 실수를 표현한다. 아 그래서 부동 소수점이 왜 부(떠다닐 부)동(움직일동) 소수점이냐구? 조금만 기다려달라. 곧 전부 얘기해줄 것이다:)
가(거짓 가) 수(셈수)는 유효숫자를 나타낸다.
1250에서 유효 숫자는 몇일까? 1250이거나 125일 수 있다.
31.4에서 유효 숫자는 몇일까? 314이다.
그럼 31.4은 부동 소수점으로 어떻게 나타낼까?
컴퓨터는 '(가수) X 2^(지수)'로 부동 소수점을 사용하지만 편의상 (가수) X 10^(지수) 형태로 생각해보자. 우린 10진법이 편하니까:)
가수부는 유효숫자를 정규화한 수다. 여기서 정규화란 N.xxx 형태로 변환시키는 작업이다.
즉 31.4은 3.14이 된다. 그럼 나머지 10^-1은 어디갈까? 그렇다. 지수로 표현된다.
이를 메모리 상에서 표현해보자!
가수부 비트에서 314를 저장하면, 이는 암묵적으로 3.14의 표현이다. 유효숫자는 가수부인 3.14가 되고 지수는 1이된다. 그림2에는 -1이라서 다르다고 생각되는가? 자연스럽다. 일부 이렇듯 지수부는 10의 거듭제곱으로 소수점의 위치를 표현한다. 이와 같은 방식을 사용하면 가수부를 넘는 유효숫자는 사용하지 못할 것이다. 1/3은 0.3333333333... 이므로 10진수 부동소수점으로 아무리 잘 표현해도 3.333333333..4 x 10^-1이 된다. 가수부가 유효숫자를 나타내므로 유한한 크기의 가수부 저장 공간은 유한 소수 밖에 표현 못하게된다.
근데 이상한 점이 있다. 앞에서 언급했듯 if (floatVariable == 0) 구문은 되도록 지양하라고 한다. 왜그럴까? [그림2] 방식으로는 0이 표현된다. 가수부가 0이면 된다. 그럼 floatVariable은 True가 될 수 있는건가? 컴퓨터의 표현 방식인 2진법 부동소수점과 10진법 부동 소수점의 차이가 있어서 그런가?
정답부터 얘기하자면
floatVariable은 True가 될 수도 있고 아닐 수도 있다!
더 헷갈리는가? 그럼 컴퓨터의 실수 표현 방식인 2진법 부동소수점을 살펴보자!
2진법 부동 소수점은 '(가수) X 2^(지수)'로 표현된다. 십진수의 부동소수점과 똑같이 가수부는 유효 숫자, 지수부는 소수점 자리수를 표시합니다. 실제로 어떻게 표시되는지 볼까요?
263.3을 '(가수) X 2^(지수)' 방식으로 표현해봅시다!
- 263은 100000111(2)
- 0.3은 01001100110011..(2)이 됩니다.
- 0.3은 2진법으로 표현하면 무한 소수 형태의 이진수가 됩니다. 0011이 반복되죠.
그렇습니다. 소수부가 무한 이진수 형태로 반복되면 정확도에 문제가 생깁니다. 0.999999...는 1이 아니지만 우리는 1이라고 표현합니다. 같은 원인, 같은 해결방안, 같은 문제인거죠. 따라서 floatVariable이 이전 연산 과정에서 정확히 0이 안될 수가 있습니다. 미세한 오차가 존재하여 0이 될 수가 있죠.
아래와 같은 상황을 가정하죠!
우린 fzero에 0.13f을 넣었습니다. 해당 변수에 0.13을 빼면, 우린 fzero가 0이 될거라고 예상할 수 있죠. 하지만 결과는 Not accurate입니다. 왜일까요? 그렇습니다 자바에서는 소수가 기본적으로 double로 표현되기 때문에 아래 식은 실제로 0.13f - 0.13d를 표현합니다. 두 실수는 유효 숫자가 다르기 때문에 같은 수가 아닙니다. 유효 숫자의 차이로 오차가 발생하게 되죠.
public static void main(String[] args)
{
float fzero = 0.13f;
if (fzero - 0.13 == 0)
System.out.println("Accurate!");
else
System.out.println("Not accurate!");
// 결과 = Not accurate!
}
위 결과는 0.13f - 0.13f 식으로 바꾸면 얻게되는 출력으로 확언할 수 있습니다
public static void main(String[] args)
{
float fzero = 0.13f;
if (fzero - 0.13f == 0)
System.out.println("Accurate!");
else
System.out.println("Not accurate!");
// 결과 = Accurate!
}
그럼 float과 Double 연산은 무조건 오차가 발생할까요?
사실 또 그렇진 않습니다. 아래와 같이 0.25f - 0.25d == 0은 true를 리턴하죠 ㅎㅎ 아래 결과는 0.13과 0.25의 소수부 표현 방식의 차이로 인해 나왔습니다. 0.13은 소수부가 무한 이진수 형태이지만 0.25는 float이든 double이든 짧은 유한 이진수 형태이기 때문입니다. 0.25는 이진수로 01이죠. 메모리 상으론 01을 제외하고 전부 0으로 채워지기 때문에 float과 double 연산임에도 제대로된 결과가 나오죠!
public static void main(String[] args)
{
float fzero = 0.25f;
if (fzero - 0.25d == 0)
System.out.println("Accurate!");
else
System.out.println("Not accurate!");
// 결과 = Accurate!
}
그렇다면 float floatVariable = 0f로 표현 후에 바로 비교하면 어떻게 될까요? True 즉, Accurate가 출력됩니다! float형 0과 double형 0과 비교해도 둘다 Accurate죠! 왜냐하면 0은 무한 이진 수로 이뤄진 가수부를 안가지기 때문이죠! 이는 당연히 float과 double 동일합니다!
public static void main(String[] args)
{
float dzero = 0f;
if (dzero == 0f)
System.out.println("Accurate!");
else
System.out.println("Not accurate!");
// 결과 = Accurate!
}
public static void main(String[] args)
{
float dzero = 0f;
if (dzero == 0d)
System.out.println("Accurate!");
else
System.out.println("Not accurate!");
// 결과 = Accurate!
}
그럼 누군가는 궁금해할겁니다. 잘만 쓰면 float로 정확도 있게 계산할 수 있네요?
정답을 얘기하면, Yes입니다.
하지만 추천하진 않습니다. 사람은 완벽하지 않으니까요.
계산 과정에서 무한 이진수가 들어간 계산이 들어가지 않을거라고 자신하시나요? '절대'는 없습니다. 안전한 많은 방법을 두고도 비효율적인 방법을 추구하는 것 만큼 아쉬운 상황은 없죠:)
그럼 돈 계산 과정에선 어떻게하죠?
실제로 돈 계산 과정은 고도의 정확성을 요구합니다. 이때 float과 double을 사용하면 안좋은 결과를 초래할 수 있죠. 이에 오라클은 돈 계산 시 BigDecimal 클래스를 사용하라고 알려줍니다. 출처: blog.oracle
float과 double을 이용한 계산은 계산 속도마저 느립니다. 정수부 사이의 계산은 두 이진수로 바로 비교하면 되지만, float과 double을 이용한 계산은 다단계로 진행합니다. 첫째로 상대 지수부에 맞춰 가수부를 변경해야하고, 둘째로 가수부끼리 연산을하고, 마지막으로 지수부와 가수부를 다시 설정해야합니다. 크게 3가지 과정이 있지만 둘째 과정만 실질적인 연산이고 나머지 둘은 부동 소수점 표현을 위한 연산입니다. 이러한 연산 비효율성으로 인해 돈 계산 시에도 실수가 쓰인다면 10^n을 곱해서 정수로 계산 후 다시 나눠주는걸 사람들은 추천합니다.
참고로 263.3은 실제로
0, 10000111, 000001110100110011...로 저장됩니다.
- 앞 0은 부호비트입니다.
- 10000111은 지수부이며 127 + 8입니다. 2^8인데 왜 127이 있냐고요? 8비트로 음수와 양수를 전부 표현하기 위해 00000000를 -127, 11111111를 128로 설정하기로 약속했습니다. 따라서 2^8을 표현하려면 10000000 + 00000111을 사용해야하죠.
- 가수부는 100000111/0100110011...이면서 왜 0으로 시작하냐구요? 이진 표현의 맨 앞은 항상 1이기 때문에 1은 굳이 표현안해주는겁니다. 00000111010011..이라고 적어도 컴퓨터는 (1)00000111010011..이라고 알아듣는거죠!
0은 맨앞이 0이기 때문에 1이 아니라고요?
좋은 추론입니다! 이런 경우는 어떻게 될까요?
독자분이 한번 알아보시는 것도 재밌을 것 같군요 ㅎㅎ
'Computer launguage > Java' 카테고리의 다른 글
자바 NIO (1) 채널, 버퍼의 동작 과정을 간단한 채팅 서버/클라 애플리케이션 구현을 통해 리서치 (0) | 2024.09.01 |
---|---|
제한된 메모리 크기 환경에서의 LinkedList와 HashMap 튜닝 방법 (0) | 2022.07.03 |
[Deep Dive] Garbage Collector(GC) 구조? 동작 과정? SE7, 8 차이? (0) | 2022.03.11 |
[Deep dive] JVM 구조? 자바 애플리케이션 실행 과정? 컴파일 과정? (2) | 2022.03.05 |
객체지향 프로그래밍? 장단점? 4대 원칙(추상화, 캡슐화, 상속, 다형성)이란? (0) | 2022.02.25 |