본문 바로가기
CS

0.1 + 0.2 != 0.3 ... ?

안녕하십니까.

커피를 내리다가 뜨거워서 놓친 하프 입니다. ☕

 

이번에는 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 이다.