자바스크립트의 숫자 자료형의 부동소수점 이슈에 관한 내용을 보게 되었다. 사실 오차가 발생한다더라 정도에서 왜? 에 대한 질문에 항상 답을 찾아보지 않고 넘어가곤 했다. 이 포스팅을 통해 부동소수점이 무엇이고 오차가 발생하는 이유가 무엇인지 정리하고자 한다.
부동소수점이란
부동소수점 또는 떠돌이 소수점 방식은 실수를 표현할 때 소수점의 위치를 고정하지 않고 그 위치를 나타내는 수롤 따로 적는 것으로, 유효숫자를 나타내는 가수와 소수점의 위치를 풀이하는 지수로 나누어 포현한다.
컴퓨터에서는 고정 소수점 방식보다 넓은 범위의 수를 나타낼 수 있어 과학기술 계산에 많이 이용되지만 근사값으로 표현되며 고정 소수점 방식보다 연산속도가 느리기 때문에 별도의 전용 연산장치를 두는 경우가 많다. 고정 소수점과 달리 정수 부분과 소수 부분의 자릿수가 일정하지 않으나, 유효 숫자의 자릿수는 정해져 있다.
원리
부동 소스점 표현 방식은 수를 (기수)x(밑수)^(지수)와 같이 유효숫자를 사용한 곱셈 형태로 표현한다. 예를 들어, -0.4를 밑수가 10인 부동 소수점으로 나타내면 -0.04x10^1이 되며 밑수가 2이면 -0.8 * 2^-1가 되는데, 가수 부분을 한자리 자연수를 갖도록 바꾸면 -4*10^-1과 같이 된다. 이처럼 가수의 첫째 자리가 밑수보다 작은 한자리 자연수가 되도록 바꾸는 것을 정규화라고한다.
컴퓨터 프로그래밍이나 전자계산기 등에서는 밑수가 10인경우에 로마자 E 또는 e를 사용하여 함수 형태로 표시하기도 하는데, -0.4는 -0.04E+1 또는 -0.04e+1이 되며, 정규화하면 -4E-1 또는 -4e-1로 쓴다. 만약, 사용할 밑수를 미리 정해 놓는다면 가수와 지수만으로 실수를 표현할 수 있는데, 앞의 보기에서 밑수를 10으로 고정한다면 실수 -0.4는 가수 -4와 지수 -1의 조합으로 나타낼 수 있다.
실제 컴퓨터에서는 보통 이진법을 사용하여 밑수를 하고, 다음과 같이 세 부분으로 실수를 나타낸다.
- 부호부 (1비트)
- 지수부(부호가 있는 정수)
- 가수부(부호가 없는 정수)
이때, 값을 먼저 이진수로 변환한 후에 정규화하고, 그것을 다시 정형화된 형식으로 표현한다. 먼저 부호는 1비트로 실수가 양수일 때는 0, 음수일 때는 1로 표현한다. 지수부에는 따로 부호 비트가 없기 때문에 음수 지수를 처리하기 위해 보통 바이어스 표현법을 사용한다. 즉, 할당된 자릿수로 표현 가능한 전체 영역을 반으로 나누어, 작은 영역에는 음수값 및 0, 큰 영역에는 양수값이 차례대로 1:1로 대응되도록 한다. 예를 들어, 지수부를 8비트로 표현한다면 모두 256가지를 나타낼 수 있는데 이것을 반으로 나누어 음수 127개와 0, 양수 128개를 차례대로 대응시킨다.
따라서 비트열 00000000은 지수 -127을 나타내고, 01111111은 지수 0 11111111은 지수 128을 나타낸다. 일반적으로 지수부가 n비트일 때 (2^n / 2 -1 = 2^(n-1) - 1)을 지수 값에 더하며 이것을 바이어스 상수라고 한다. 가수부에는 정규화 결과 유효숫자의 첫째 자리는 언제나 1이므로 표시하지 않고, 소수 부분만 표현한다.
정확도 문제
부동 소수점으로 표현한 수가 실수를 정확하게 표현하지 못하고 부동 소수점 연산 역시 실제 수학적 연산을 정확히 표현하지 못하는 것은 여러가지 문제를 낳는다.
예를 들어, 0.1과 0.01을 표현하지 못하므로 0.1의 제곱이 0.01이 되지도 않고 0.01과 가장 가까운 수가 되지도 않는다. 24비트 단정밀도 표현에서 십진수 0.1은 지수 = -4: 가수 110011001100110011001101 이고 그 값은,
정확히 0.1000000001490116193847656256 이다.
이 수를 다시 제곱하면,
정확히 0.01000000029802322609973991742503130808472633366181640625 이다.
단정밀도 부동 소수점 (반올림이 있는) 하드웨어에서 제곱을 한다면,
정확히 0.01000000000707805156707763671875 이다.
하지만 0.01과 가장 가까운 표현 가능한 숫자는
정확히 0.0099999999776482582092285156250 이다.
유효숫자를 잃어버리는 문제 뿐만 아니라, 0.1을 정확하게 표현하지 못하는 문제와 다른 약간의 부정확 성이 다음과 같은 형상을 일으킨다.
1) 소거 : 거의 같은 두 값을 빼는 것은 정확성을 매우 많이 잃게 된다. 이 문제가 아마도 가장 일반적이고 심각한 정확도 문제이다.
2) 정수로의 변환 문제 : (63.0 / 9.0)을 정수로 변환하면 7이 되지만 (0.63 / 0.09)는 6이 된다. 이는 일반적으로 반올림 대신 버림을 하기 때문이다.
3) 제한된 지수부 : 결과값이 오버플로되어 무한대값이 되거나 언더플로되어 비정상 값 또는 0 이 될 수 있다. 만약 비정상 값이 되면 유효숫자를 완전히 잃어버린다.
4) 나누셈이 안전한지 검사하는데 문제가 생김: 제수(나눗수)가 0이 아님을 검사하는 것이 나눗셈이 오버플로되고 무한대값이 되지 않는 걸 보장하지 않는다.
5) 같음을 검사하는데 문제가 생김: 수학적으로 같은 계산결과가 나오는 두 계산 순서가 다른 부동소수점 값을 만들어낼 수 있다. 프로그래머는 어느 정도의 허용 오차를 가지고 비교를 수행하지만, 그렇다고 해서 문제가 완전히 없어지지 않는다.
규격
실제 사용되고 있는 부동 소수점 방식은 대부분 IEEE 754 표준을 따른다. 이 규격에서는 실수를 32비트로 처리하는 단정밀도(single precision)에서는 부호 1비트, 지수부 8비트, 가수부 23비트를 사용하며, 64비트로 처리하는 배정밀도(double precision)에서는 부호 1비트, 지수부 11비트, 가수부 52비트를 사용한다. 4배정도(quadruple precision)에서는 128비트로 부호 1비트, 지수부 15비트, 가수부 112비트를 사용한다.