본문 바로가기
  • 파이썬 (2)
  • 환경설정 (2)
  • 주제1 (0)
  • 데이터베이스 (1)
  • CS

    0.1 + 0.2 != 0.3 ... ?

    안녕하십니까.

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

     

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