2부에서 이어지는 포스트입니다.
실수계산 오차와 부동소수점 ( feat. IEEE 754 표준 ) 2부
1부 에서 이어지는 포스트입니다. 실수계산 오차와 부동소수점 ( feat. IEEE 754 표준 ) 1부영어 공부겸 Medium의 개발 블로그를 탐방하던 도중에 이런 포스트를 찾았습니다.그리고 최근 개발 커뮤니티
forrest-code-13.tistory.com
이번 포스트에서는 나머지 궁금증을 해결해보도록 하겠습니다.
BigDecimal 클래스
이전 포스트에서 설명했듯이, 컴퓨터가 실수를 이진수로 변환하는 과정에서 발생하는 무한소수로 인해 오차가 발생합니다. 이런 이진수 변환에 의한 오차를 막기위해 사용되는 클래스가 BigDecimal 클래스입니다.
BigDecimal 내부구현
BigDecimal 클래스는 실수를 이진수 변환 없이 정확한 10진수(decimal) 형태로 저장합니다.
실수를 정수 형태로 저장하기 위해 BigDecimal은 부동소수점 표현 중 소수점 아래가 없는, 예를 들어 ( 12345 , -2 )같은 표현을 사용합니다. 가수( 12345 )는 intVal에, 지수( -2 )는 scale에 저장되며 저장공간의 부족으로 인한 오차를 예방하고자 intVal 필드는 BigInteger 클래스로 선언합니다.
다음은 BigDecimal의 내부 구조의 일부입니다. ( 값을 표현할 때는 scale에 ' - ' 를 붙여서 표현합니다. 그 이유는 scale이 "소수점 아래자리의 개수"를 뜻하기 때문입니다. )
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; // 12345
private final int scale; // 2
// 12345 * 10^(-2)
//...
}
System.out.println(0.1)의 출력 " 0.1 "
분명히 컴퓨터는 0.1을 저장할 때 오차가 생긴다고 설명했습니다. 그런데 별도의 연산 없이 0.1을 출력하면 또 잘 나옵니다. 그 이유는 println함수의 내부 구현에 있습니다.
println함수를 쭉 따라가면 String.valueOf(double) -> Double.toString(double) -> DoubleToDecimal.toString(double) -> DoubleToDecimal(false).toDecimalString(double) -> toDecimal(double,null) 로 이어지며, 다음은 toDecimal(double,null) 함수의 내부 모습입니다.
private int toDecimal(double v, FormattedFPDecimal fd) {
long bits = doubleToRawLongBits(v);
long t = bits & T_MASK;
int bq = (int) (bits >>> P - 1) & BQ_MASK;
//...
위 코드에서 P라는 값이 보이는데 이 값은 double 자료형이 저장할 수 있는 실수의 정밀도를 뜻합니다. println함수는 정밀도를 기준으로 반올림을 진행하기 때문에 0.1이라는 실수를 출력해도 0.10000000000000000555가 아닌, 반올림한 0.1이 출력됩니다.
추가내용. printf함수로 출력하는 float과 double
이번 포스트를 준비하면서 여러가지 실험을 하던 도중 다음 출력을 보고 정신이 아찔했습니다.
float a = 0.1f;
System.out.printf("%.20f\n", a); // 0.10000000149011612000
System.out.println(a); // 0.1
double b = 0.1;
System.out.printf("%.20f\n", b); // 0.10000000000000000000
System.out.println(b); // 0.1
println함수는 인자의 타입별로 오버라이딩이 되어 정밀도를 다르게 설정합니다. 그래서 적당한 자리에서 반올림되어 둘다 0.1이 출력되됩니다. 하지만 printf함수의 출력은 정말 생각지도 못한 출력이었습니다.
자료를 찾아보면서 printf는 인자로 들어온 float를 double으로 자동 형변환(casting)을 한다는 것을 알게 되었습니다. 그래서 float으로 저장된 값이지만, 반올림이 double 자료형과 같이 소수점 17번재 자리에서 일어나는 것이었습니다.
printf 함수의 float 변수 출력과정
- float의 정밀도는 7이기 때문에 소수점 8번째 자리에서부터 오차 발생
- float -> double로 형변환
- double의 정밀도 위치에서 반올림 (소수점 17번째 자리)
- 8자리 ~ 16자리까지의 오차는 유지되고 이후 오차는 제거된다.
- 반올림의 결과 : 0.10000000149011612000
- 8자리 ~ 16자리까지의 오차 : 149011612
- 출력 : 0.10000000149011612000
End.
참고자료
- https://dev.gmarket.com/75
- https://jsonobject.tistory.com/466
- https://developer-hm.tistory.com/229
- https://forum.arduino.cc/t/understanding-print-double-precision-float-0-1/1267173
- https://stackoverflow.com/questions/33832574/floating-point-precision-and-equality-in-java
'CS' 카테고리의 다른 글
DI의 종류와 Constructor DI가 권장되는 이유 (4) | 2025.06.14 |
---|---|
Java final 키워드 (1) | 2025.06.12 |
실수계산 오차와 부동소수점 ( feat. IEEE 754 표준 ) 2부 (0) | 2025.05.16 |
URI vs URL vs URN (0) | 2025.04.14 |
REST API에서 "REST"의 의미 (0) | 2025.04.12 |