Java : 부동소수점
부동소수점
codeup 1023번 문제를 풀다가 실수 연산 중 부동소수점 때문에 한참을 고생해서 포스팅합니다!
부동 소수점은 가수부와 지수부로 나누어서 저장을 합니다. 즉 (a)*2^b
꼴로 저장을 하는데요. 이 때, a는 1보다 크거나 같고 2보다 작은 실수입니다. 즉 (1.xxx)*2^b
꼴로 저장을 한는 것입니다.
보통 부동 소수점, 우리가 흔히 알고 있는 float이나 double형은 이런 식으로 저장이 됩니다. 부호(sign), 지수부(exp), 가수부(fraction)로 나누어서 저장을 하고 있는데요. 이 fraction 부분은 (1.xxx)*2^b
로 표현했을 때, 0.xxx
부분을 저장한다고 보시면 됩니다. 0.xxx
를 2진수로 표현해서 저장하게 됩니다.
앞에서 0번째 bit의 자릿수는 1/2를, 1번째 bit의 자릿수는 1/4를, … , k번째 bit의 자릿수는 (1/2^(k+1))
를 나타냅니다. 즉 부동 소수점형 0.1의 가수부(fraction) 부분은 1/2 + 1/8 + 1/16 + ...
이런 식으로 표현이 됩니다. 그렇기 때문에 가수부(fraction)는 t/2^u
꼴로 표현이 되거나, 0이여야 합니다. 그렇지 않다면 2진수로 표현했을 때, 무한 소수로 표현이 될 수 밖에 없습니다.
예제 비교
이 프로그램의 결과는 어떻게 나올까요? 0.01을 10000번 더했으니까, 100이 출력이 될까요?
아닙니다. 0.01은 t/2^u
꼴로 표현될 수 없어, 무한 소수로 표현되고 오차가 발생합니다. 때문에, 오차가 있는 결과값을 10000번 더해도 실제 결과값과 차이가 나게 됩니다.
그러면 부동 소수점은 어떤 꼴의 실수를 표현할 때 정확하게 표현할 수 있을까요? 0.625는 어떨까요?
0.625는 2^-1
에 (1+0.25)를 곱한 값으로 표현할 수 있습니다. 즉 가수부(fraction)는 1/4입니다. 이는 이진수 0100
로 표현할 수 있습니다. 딱 나누어 떨어집니다. 따라서 0.625는 부동 소수점으로 오차 없이 정확하게 표현할 수 있습니다.
double과 float의 비교연산시 문제
float는 4바이트 실수, double은 8바이트 실수 값을 저장 할 수 있습니다. 부동소수점으로 실수를 표현하기 때문에 double은 float보다 상대적으로 보다 더 정확한 실수를 표현 할 수 있습니다.
System.out.println(1.0 == 1.0f); // 결과 : true
System.out.println(1.1 == 1.1f); // 결과 : false
System.out.println(0.1 == 0.1f); // 결과 : false
System.out.println(0.9 == 0.9f); // 결과 : false
System.out.println(0.01 == 0.01f); // 결과 : false
위 예제를 눈으로면 보면 다 true일 것이라 착각 하기 쉽습니다. 하지만 주석처럼 맨 윗줄만 제외하고 다 false를 출력합니다. 이유는 눈에 보이지는 않지만 float와 double자료형의 실수 표현의 정밀도의 차이가 발생하기 때문입니다.
System.out.println((float)1.1 == 1.1f); // 결과 : true
System.out.println(0.1 == (double)0.1f); // 결과 : false
System.out.println(0.1f == (double)0.1f); // 결과 : true
주의 할 점은 0.1f를 double로 형변환 하여 비교해도 될 거라 생각 할 수 있는데, (double)0.1f는 double의 공간에 float의 정밀도를 갖는 값이 저장될 뿐입니다. 따라서 double형의 0.1과 비교해도 결과가 true로 나올 수 없습니다.
보동소수점 오류
모든 실수를 8byte, 혹은 12~16byte의 변수에 모두 담을 수 있없습니다. 그렇기 때문에 변수에 실수를 저장할 때는 어느 정도의 정보 손실이 일어날 수 밖에 없습니다. 절대 잊어서는 안되는 것은, 실수 변수는 절대 정확한 값을 가지고 있지 않다는 것입니다.
오차때문에 틀리는 예시 몇가지
2번 때문에 틀리는 경우가 많으니 꼭 확인을 해야합니다.
1) 문제를 풀 때는 float보다는 double형 변수를 쓰는게 좋습니다.
double형 변수까지는 하드웨어로 계산되기 때문에 많이 느려지지 않지만, 정확도가 엄청나게 높아지기 때문입니다.
(float의 상대오차는 약 10^-7 정도이고, double의 상대 오차는 약 10^-15 정도입니다)
long double (12bit 혹은 16bit)는 소프트웨어의 도움을 받기 때문에 꽤 많이 느려지기 때문에 쓴다고 꼭 좋은 것은 아닙니다.
2) 정수가 들어있는 실수형 변수를 정수로 바로 캐스팅하면 안됩니다.
1을 double 변수에 대입하면 0.9999… 같은 이상한 숫자가 됩니다. 이 변수를 그대로 정수로 캐스팅한다면 0이 됩니다.
보통의 경우에는 1e-6 ~ 1e-9 정도를 더해서 캐스팅을 하거나 정수형 변수만을 사용하여 연산을 하기도 합니다.
( scanf(“%d.%d”)으로 입력을 받고, printf(“%d.%02d”, a/100, a%100)으로 출력하는 식으로.반올림이 필요하다면 나머지 연산을 사용해야 합니다.)
3) 비교 연산을 할 때는 등호를 사용하시면 안됩니다.
실수형 변수는 오차가 있기 때문에 같은 값을 가져야만 하는 상황에서도 다른 값일 때가 매우 많습니다.
보통의 경우에는 abs(A-B) < EPS, EPS는 1e-6~1e-9 정도로 정합니다.
4) 큰 수를 다룰 때, 매우 작은 상수값을 사용하는 것은 위험할 수 있습니다.
double형의 상대 오차는 10^-15입니다. 즉, 10^15를 double형 변수에 대입하면 오차가 1의 자리에서 발생할 수 있습니다.
즉, 조건문으로 (A-B) < 1e-6 을 사용했는데 A, B가 10^11 크기 정도라면, (A-B) < 1e-6과 A == B는 똑같은 결과를 만들게 됩니다.
double형 변수를 가지고 넓은 범위의 이진탐색을 돌릴 때 자주 발생하는 문제이고 100~200번 정도만 반복한다던가 상대오차가 몇 이하일 때 반복문을 빠져나오는 식으로 해결합니다.
5) 큰 수에 작은 수를 더할 때 조심해야 합니다.
예를 들어, 10^20 정도 되는 double형 변수에 1을 10^20번 더해도 값이 변하지 않습니다. 또는, 큰 수에 작은 수를 더할 때 작은 수의 정밀한 부분이 사라지기 때문에 오차가 커질 수 있습니다.
보통은 작은 수끼리 더한 뒤에 큰 수에 더하는 방법으로 해결이 가능하나 이런 것 때문에 틀리는 문제는 극히 드뭅니다.
이렇게 부동소수점에 대해 알아봤는데요! 위에서 본 것처럼 실수를 정확하게 계산하는 것은 힘들까요? 아닙니다! Java에서 실수를 정확히 계산하기 위한 BigDecimal 이라는 클래스가 있습니다!
참고 : https://www.acmicpc.net/blog/view/37
https://www.hanumoka.net/2017/09/08/java-20170908-java-floating-point-issue/
https://webheck.tistory.com/entry/Java-%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90-%EC%98%A4%EB%A5%98
https://codingdog.tistory.com/entry/%EB%B6%80%EB%8F%99-%EC%86%8C%EC%88%98%EC%A0%90-%EC%99%9C-01%EC%9D%84-%EC%A0%80%EC%9E%A5%ED%95%98%EB%A9%B4-%EC%98%A4%EC%B0%A8%EA%B0%80-%EC%83%9D%EA%B8%B8%EA%B9%8C%EC%9A%94
댓글남기기