title | date | author | category |
---|---|---|---|
0.1 + 0.2 !== 0.3 인 이유 (ieee 754) |
2020-07-07 17:00:00 -0700 |
jinsunee |
Javascript |
10진수 계산에 익숙해져 있는 우리는 단번에 이해하기 어려운 결과일 것 입니다. 그렇지만 저 계산은 Javascript engine 에 의해 돌아가는 브라우저 console에서 계산이 되어진 것이기 때문에 그에 대한 좀 더 내부적인 관점에서의 해석이 필요합니다. 이러한 계산 결과는 Javascript를 포함한 C, Java 등에서도 발견할 수 있는데 결론적으로,
컴퓨터에서
숫자와 숫자를 더한다는 것
은컴퓨터에 저장된 숫자와 컴퓨터에 저장된 숫자
를 더하는 것이므로, 저장방식에 따른 오차가 생겨 위와 같은 결과가 나온 것입니다.
이제부터 컴퓨터가 숫자를 저장하고 표현하는 방식을 알아보도록 하겠습니다.
- 고정 소수점(Fixed Point) 표현 방식 : 정확성이 떨어지기 때문에 높은 정밀도가 필요없는 소규모 시스템에서 간혹 쓰인다.
- 부동 소수점(Floating Point) 표현 방식 : 고정 소수점에 비해 추가적인 연산이 필요하므로 속도가 느리다. 그렇지만 보다 더 넒은 범위의 수를 나타낼 수 있으므로 많이 사용된다.
큰 틀은 다음과 같습니다.
이진 기수법
을 사용하여10진수 -> 2진수
로 바꾼다.고정 소수점
혹은부동 소수점
방식을 사용하여 숫자를 저장하고 표현한다.
예를 통해서 알아보겠습니다.
10진수가 이진 기수법에 의하여 2진수로 바뀐 결과로 시작해보겠습니다.
ex) 43.6875(10) = 101011.1011(2)
2진수 변환 -> 표현/저장
말 그대로 소수점이 고정된 상태로 표현되며 저장되는 방식입니다.
- 부호 : 1bit (양수: 0, 음수: 1)
- 정수 : 15bits
- 소수 : 16bits
소수부에 할당된 비트 숫자에 따라 정밀도가 달라지지만, 표현 가능한 실수의 범위와 정밀도가 제한적이기 때문에 실질적으로 잘 사용하지 않습니다.
2진수 변환
-> 정규화
-> 표현/저장
2진수에서 정규화를 거쳐 유효 숫자 가수
와 소수점에 대한 정보를 담은 숫자 지수
, 그리고 부호를 표현하는 비트로 표현하는 방식입니다.
-
2진수 변환
43.6875(10) = 101011.1011(2) -
정규화
2진수 ->1.xxxx * 2^n
꼴로 바꾸는 것 여기서
xxxxx
: 유효 숫자, 가수부n
: bias, 지수부를 구성하는 숫자
-
- 32bits 저장 방식: 단정도 방식 - 부호: 1bit - 지수부: 8bits - 가수부: 23bits - 64bits 저장 방식: 배정도 방식 - 부호: 1bit - 지수: 11bits - 가수: 52bits
43.6875(10) = 101011.1011(2) 에서의 표현으로 예를 들어보겠습니다.
- 부호: 0
- 지수부: 1111010(2)
지수 = bias + 매직넘버(32bits의 경우 127이라고 규정)
이므로, -5 + 127 = 122(10) = 1111010(2) - 소수부: 10111011(2)
자바스크립트에서는 64bit 부동소수점으로 숫자를 표현하고 있습니다.
-
먼저 2진수로 변환해보겠습니다.
0.1(10) => 0.0001100110011001100110011001100110011001100110011001100110011…….(2)
0.2(10) => 0.0011001100110011001100110011001100110011001100110011001100110…….(2)
-
정규화 과정을 거치면
0.1(10) => 1.1001 1001 1001 1001 1001 1001………(2) * - (2 « 3) // 2의 -4승
0.2(10) => 1.1001 1001 1001 1001 1001 1001………(2) * - (2 « 2) // 2의 -3승
-
위와 같이 계산한다면 최종적으로 저장되는 값은 아래와 같이 가수부분의 마지막이 반올림되는데,
0.1(10) => 0 1111011 10011001100110011001101
0.2(10) => 0 1111100 10011001100110011001101
이 부분이 0.1 + 0.2 !== 0.3
의 원인이 됩니다.
// 0.1을 변환
// 먼저 가수부의 값을 가져와서 생략되어진 1을 추가합니다. 그리고 10진수로 변경합니다.
var mantissa = parseInt('1' + '10011001100110011001101', 2); // 13421773
var exponent = -4;
// 그리고, 1.10011001100110011001101 이 아닌
// 110011001100110011001101을 연산했으므로 -23의 지수가 추가됩니다.
exponent += -23;
mantissa * Math.pow(2, exponent);
// 7.450580596923828e-9 그리고 두 개의 값을 곱해줍니다.
var result = mantissa * exponent;
// 0.10000000149011612 의 결과값이 보입니다.
// 0.2을 변환
// 먼저 가수부의 값을 가져와서 생략되어진 1을 추가합니다. 그리고 10진수로 변경합니다.
var mantissa = parseInt('1' + '10011001100110011001101', 2); // 13421773
var exponent = -3;
// 그리고, 1.10011001100110011001101 이 아닌
// 110011001100110011001101을 연산했으므로 -23의 지수가 추가됩니다.
exponent += -23;
mantissa * Math.pow(2, exponent);
// 1.4901161193847656e-8 그리고 두 개의 값을 곱해줍니다.
var result = mantissa * exponent;
// 0.20000000298023224 의 결과값이 보입니다.
// 즉, 그대로 다시 10진수로 변환해보면 아래와 같이 0.1과 다른, 0.2와 다른 값이 출력됩니다.
var number1 = 0.10000000149011612;
var number2 = 0.20000000298023224;
// 따라서, 0.1 + 0.2 값도 다를 수 밖에 없습니다.
console.log(number1 + number2); // 0.30000000447034836
이러한 이유로 0.1 + 0.2의 결과값은 다를 수 밖에 없습니다. 이를 유의하여 사용할때에는 소수점을 고정시키는 방법을 고려해야 합니다.
var number1 = 0.10000000149011612;
var number2 = 0.20000000298023224;
var result = (number1 + number2).toFixed(1);
console.log(result); // 0.3
https://imcts.github.io/javascript-exponent-expression/ https://gsmesie692.tistory.com/94 https://hoodymong.tistory.com/50