안녕하십니까.
커피를 내리다가 뜨거워서 놓친 하프 입니다. ☕
이번에는 0.1 과 0.2 더한값이 0.3 이 아닌 이유를 설명해보겠습니다.
개발자 면접 질문에서도 많이 나오는 질문이기도 하죠!
0.1 + 0.2 != 0.3
원인 파악
수학적으로 생각하여 보면, 발생할 수 없는 오류일 것 입니다.
하지만 컴퓨터의 동작원리로 본다면 달라질 수 밖에 없는 구조입니다.
간단하게 개발자 모드에서도 확인할 수 있습니다.
결과를 보니 0.30000000000000004
이 나오는걸 볼 수 있습니다.
0 이 왜이렇게 많을걸까요 ㅋㅋ ..
데이터 저장 방식
우선 컴퓨터가 어떻게 데이터를 저장하는지 알아보겠습니다.
보통의 언어에서는 float : 32bit, double : 64bit로 되어 있고
자바스크립트의 경우 number : 64bit로 되어 있습니다.
그렇다는건 데이터를 저장할 수 있는 공간은 한정적
이라는 것이죠.
32bit를 IEEE 754
기준으로 설명드리겠습니다.
넣을 데이터는 15.03125(10) 로 해보겠습니다.
정수와 소수로 먼저 나누겠습니다.
정수부 : 15
소수부 : 03125
2진수로 변환을 하면 1111 . 0000 1000
이렇게 될 것 입니다.
여기에서 .
부분을 맨 앞으로 땡겨 옵니다.
그러면 1.1110001000 x 2^3
이렇게 해야 위의 결과와 똑같이 나오겠죠!
소수점 뒤에있는 1110001000
이 부분을mantissa
이라고 합니다.
아래의 표를 보시죠!
맨 첫 번째는 부호비트 입니다.
양수일 경우는 0, 음수일 경우는 1 입니다.
앞에 부호비트를 제외한 8칸은 일단 비워두고 mantissa 부분을 넣어줍니다.
0 | |||||||
1 | 1 | 1 | 0 | 0 | 0 | 1 | |
0 | 0 | 0 | |||||
그런다음 앞에 8칸은 지수부분 2^3
을 127에 더해줍니다.
왜 127을 더해주냐!?
기수부분을 표기하기 위해 존재한다고 보시면 되겠습니다.
부동 소수점에서 32bit 일때는127
을, 64bit 일때는 1023
을 더해줍니다
그리고 이걸바이어스
라고 부릅니다.
지수가 3이니, 127 + 3 = 130 = 10000010(2)
이 부분을 8칸에 채워줍니다!
0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
0 | 0 | 0 | |||||
이렇게 컴퓨터가 데이터를 저장한다고 보시면 되겠습니다!
원인 재현
원인을 재현을 해보기 위해 0.1을 2진수로 변경해보겠습니다.
변경하는 법을 잘 모르시는분은 여기 먼저 확인해주시면 감사하겠습니다!
0.1을 2진수로 변경을 하게 되면..
0.00011001100110011...
이러한 무한 소수가 될 것 입니다.
0.2또한 2진수로 변경을 하게 되면..
0.00110011001100110...
이러한 무한 소수가 될 것 입니다.
컴퓨터의 경우 이럴경우 자를 수 밖에 없습니다 🥹
위의 방식과 같은 방법으로 저장을 해보겠습니다.
양수 : 0
지수부 : 1023 - 4 = 1019(01111111011)
-4인 이유는 2^(-4) 이기 때문!
mantissa : 1.10011001100110011...
무한 소수라서 자를 수 밖에 없습니다 !...
0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
즉, 그 이 밑의 있는 값들만큼 잘려나감으로써.. 그만큼의 오차가 있다고 보시면 됩니다!...
마지막 10은 잘려나감으로써, 반올림 되었다고 생각하시면 되겠습니다!
이걸 계산해보면!..
네... 뭐 그렇다고 합니다
대충 이걸 코드로 계산해본다 하면..
// 주어진 64비트 이진수
var binary = "0011111110111001100110011001100110011001100110011001100110011010";
// 부호 비트
var sign = (binary[0] === "1") ? -1 : 1;
// 지수 비트
var exponent = parseInt(binary.substring(1, 12), 2) - 1023;
// 가수 비트
var fraction = binary.substring(12);
// 가수를 십진수로 변환
var fractionSum = 0;
for (var i = 0; i < fraction.length; i++) {
fractionSum += parseInt(fraction[i]) * Math.pow(2, -(i + 1));
}
// 결과 계산
var result = sign * (1 + fractionSum) * Math.pow(2, exponent);
console.log(result);
0.1 ...!
0.1이 이렇게 귀할줄은 몰랐네요
0.2도 똑같이 해본다면 ..
양수 : 0
지수부 : 1023 - 3 = 1020(01111111100)
mantissa : 1.10011001100110011...
0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
// 주어진 64비트 이진수
var binary = "0011111111001001100110011001100110011001100110011001100110011010";
// 부호 비트
var sign = (binary[0] === "1") ? -1 : 1;
// 지수 비트
var exponent = parseInt(binary.substring(1, 12), 2) - 1023;
// 가수 비트
var fraction = binary.substring(12);
// 가수를 십진수로 변환
var fractionSum = 0;
for (var i = 0; i < fraction.length; i++) {
fractionSum += parseInt(fraction[i]) * Math.pow(2, -(i + 1));
}
// 결과 계산
var result = sign * (1 + fractionSum) * Math.pow(2, exponent);
console.log(result);
0.2 !!!
깔끔하게 나왔네요
0.1과 0.2를 더해봅시다.
function addBinaryFloats(binary1, binary2) {
// 부호를 확인
var sign1 = parseInt(binary1[0]);
var sign2 = parseInt(binary2[0]);
// 두 개의 부호가 같은지 확인
var signResult = sign1 === sign2 ? 0 : 1;
// 지수를 가져오고 비교
var exponent1 = parseInt(binary1.substring(1, 12), 2);
var exponent2 = parseInt(binary2.substring(1, 12), 2);
// 가수를 가져오기
var fraction1 = '1' + binary1.substring(12);
var fraction2 = '1' + binary2.substring(12);
// 더 큰 지수에 맞추기
var maxExponent = Math.max(exponent1, exponent2);
var shiftAmount1 = maxExponent - exponent1;
var shiftAmount2 = maxExponent - exponent2;
// 가수를 적절히 이동
if (shiftAmount1 > 0) {
fraction1 = fraction1.padEnd(fraction1.length + shiftAmount1, '0');
} else if (shiftAmount2 > 0) {
fraction2 = fraction2.padEnd(fraction2.length + shiftAmount2, '0');
}
// 가수 더하기
var resultFraction = (sign1 === 0 ? 1 : -1) * parseInt(fraction1, 2) + (sign2 === 0 ? 1 : -1) * parseInt(fraction2, 2);
// 결과 정규화
var resultExponent = maxExponent + 1;
var resultSign = resultFraction < 0 ? 1 : 0;
resultFraction = Math.abs(resultFraction).toString(2).substring(1);
resultFraction = resultFraction.padStart(52, '0');
// 결과 조합
var result = resultSign + resultExponent.toString(2).padStart(11, '0') + resultFraction;
return result;
}
var binary1 = "0011111110111001100110011001100110011001100110011001100110011010";
var binary2 = "0011111111001001100110011001100110011001100110011001100110011010";
var result = addBinaryFloats(binary1, binary2);
console.log(result);
해당 결과를 확인해보면
001111111101001100110011001100110011001100110011001100110011010000
이런 값이 나오게 됩니다.
이걸 다시 .. 10진수로 바꿔보면..
키야... 멋쥡니다.
드디어 구현했네요.
마지막으로 최종 결과를 확인해봅시다.
필자는 이 글을 쓰려고 몇 시간동안 컴퓨터와 싸웠다고 합니다.
끗
망할 IEEE 754
요약
0.3 은 001111111101001100110011001100110011001100110011001100110011010000 이다.
'CS' 카테고리의 다른 글
GIT Bash를 이용해서 브랜치 생성 및 머지를 해보자! (2) | 2024.06.09 |
---|---|
GIT Bash로 커밋, 푸시를 해보자! (0) | 2024.06.08 |
소수점 진법 변환을 알아보자! (2) | 2024.05.21 |
진법 변환을 알아보자 (4) | 2024.03.24 |
자바스크립트 동작 원리 (2) | 2024.03.17 |