모두가 아시다시피 컴퓨터는 이진수를 사용합니다. 그리고 하나의 데이터를 저장하는 공간은 한정되어 있습니다. 이로 인해 작은 숫자를 표현할 때는 오차가 발생하게 됩니다. 이 오차가 바로 개발자 면접의 단골질문인 0.1 + 0.2 == 0.3의 결과가 false가 되는 원인이 되는 오차입니다.

이진법으로 소수점 이하의 숫자 표현하기

이진법에서 소수점 이하의 숫자를 표현하는 방법은 십진법과 같습니다. 십진법에서 소수점 오른쪽 자리가 10의 음의 거듭제곱을 나타내듯이 소수점 오른쪽에 있는 자리는 2의 음의 거듭제곱으로 표현됩니다. 예를 들어, 0.1(이진수)는 1/2를 의미하며, 0.01(이진수)은 1/4, 0.11(이진수)은 1/2 + 1/4로 3/4을 나타냅니다. 진법의 문제로 일부 십진법 숫자들은 이진법으로 정확하게 나타내기 어려운데요. 0.1의 경우가 그렇습니다. 0.1이라는 숫자는 바로 확인할 수 있듯이 소수 첫째 자리까지 존재하는 유한소수입니다. 하지만 이를 이진법으로 나타내면 순환소수가 됩니다.

십진법을 이진법으로 변환하기

소수점 이하의 십진수 이진법으로 변환하려면 소수 부분에 2를 반복적으로 곱하며 정수부분을 가져와 자릿수에 맞게 표시하면 됩니다. 예를 들어 십진법 0.25를 이진법으로 변환하면 0.25에 2를 곱합니다. 그 값은 0.5이 되며 정수부분인 0을 가져와 소수 첫번째자리에 놓고 남은 0.5에 다시 2를 곱합니다. 그러면 그 값은 1이 되며 해당 숫자를 소수 두번째자리에 표시하고 남은 수는 0이므로 이진법 변환이 완료되는 것입니다.

excalidraw로 직접 그림

그럼 십진법 0.1을 이진법으로 나타내면 어떤 결과가 나올까요? 우선 0.1에 2를 곱하면 0.2가 되고, 이 중 정수 부분은 0입니다. 소수 부분 0.2에 다시 2를 곱하면 0.4가 되며, 정수 부분은 여전히 0입니다. 이후 0.4에 2를 곱하면 0.8이 되고, 정수 부분은 0입니다. 이 과정을 계속하면 0.8에 2를 곱해 1.6이 되며 정수 부분은 1이고, 남은 소수 부분 0.6을 다시 2로 곱하면 1.2가 되어 정수 부분 1과 소수 부분 0.2가 반복됩니다. 결과적으로 0.1은 이진법으로 0.00011001100110011…과 같은 순환 무한 소수로 표현되며, 순환 부분은 "0011"입니다. 그림으로 나타내면 다음과 같습니다.

excalidraw로 직접 그림

컴퓨터에서 실수를 저장하는 방법

자 이제 십진법 숫자를 이진법으로 전환할 때 유한소수였던 숫자가 무한소수로 표현되는 것을 알았습니다. 이제 한 가지만 더 알아보면 0.1 + 0.2 == 0.3의 결과가 false가 되는 원인을 정확하게 파악할 수 있습니다. 바로 컴퓨터가 실수를 저장하는 방법입니다. 실수를 컴퓨터에서 효율적이고 일관적으로 표현하기 위해 IEEE 754 국제 표준을 사용합니다. 이 표준은 실수를 부동소수점방식으로 저장합니다. 비트의 수에 따라 32비트 단정밀도와 64비트 배정밀도 등 여러 방식이 있지만 결국 차지하는 비트의 수의 차이로 인한 정밀도, 범위 등의 차이이므로 여기서는 32비트 단정밀도를 기준으로 설명드리겠습니다.

부동소수점(Floating Point)
부동소수점은 숫자를 표현할 때 정수부와 소수부의 위치를 고정하지 않고, 숫자를 가수(Mantissa)와 지수(Exponent)로 나누어 표현하는 방식입니다. 부동소수점에서 숫자는 ±(number)×base^k와 같은 방식으로 표현합니다.

IEEE 754의 부동소수점 표현

IEEE 754에서는 부동소수점을 표현하기 위해서 데이터 저장 공간을 세 가지로 나누어 사용합니다.

위키백과, IEEE 754

  1. 부호부
    숫자가 양수인지 음수인지 결정하는 부분으로 0이면 양수, 1이면 음수를 의미합니다. (1이 아니라 0이 양수인 이유는 2의 보수에서 유래된 개념이 이어진 것같습니다. 자세한 내용은 2의 보수와 관련된 내용을 참고하세요.)
  2. 지수부(exponent)
    숫자를 부동소수점으로 만들기 위해 정규화한 이후 실제 자리수의 정보를 나타냅니다.
  3. 가수부(Mantissa/Fraction)
    정규화된 숫자에서 일의 자리에 있는 숫자 1을 제외한 부분입니다. 아래 변환 방법을 보시면 아시겠지만 일의 자리 수는 무조건 1이 되도록 만드므로 일의 자리는 "암묵적 1"로 굳이 저장하지 않습니다.

IEEE 754로 숫자를 저장하기 위해서는 먼저 32비트의 공간을 확보합니다.(앞서 언급했듯 설명은 32비트 단정밀도 기준입니다.) 32비트의 공간은 부호부(1 비트) + 지수부(8 비트) + 가수부(23 비트)로 나눕니다. 먼저 부호부는 실제 숫자의 부호를 저장합니다. 양수면 0, 음수면 1이 됩니다. 그리고 부호를 제외한 수, 즉 절댓값을 정규화합니다.

 

정규화를 위해 먼저 저장할 숫자를 이진법으로 변환합니다. 그리고 일의 자리가 1이고 십의 자리 이상의 수가 존재하지 않도록 소수점을 왼쪽으로 옮깁니다. 예를 들어 이진법 수 1010101.1001이 있다면1.0101011001로 만드는 식입니다. 이 예시에서는 소수점을 6번 왼쪽으로 옮겼습니다. 즉 2의 6승만큼 더 곱해진 상태입니다. 여기서 6이 지수부가 되고 정규화된 결과의 소수 부분인 0101011001가 가수부가 됩니다. 지수부는 미리 정해진 만큼의 수(Bias)를 더해 해당 수가 음수인지 양수인지를 구분합니다. 32비트 단정밀도에서 Bias는 127이므로 6에 127을 더해 133을 지수부에 저장합니다. 가수부는 총 23비트인데 정규화된 수를 채운 뒤 가수부의 나머지 비트는 0으로 채워 가수부가 총 23비트로 저장될 수 있도록 합니다.

 

다음은 실수 85.5625를 IEEE 754에 저장하는 예시를 그림으로 나타낸 것입니다.

excalidraw로 직접 그림

오차가 발생하는 이유

자 여기까지 알아보았다면 오차가 발생하는 이유를 알 수 있게 되었을 것입니다. 오차의 원인은 바로 가수부의 비트 크기입니다. 처음에 설명했든 0.1은 순환소수가 됩니다. 즉 0.00011001100110011…을 정규화한다면 1.1001100110011…이 되는데 32비트 단정밀도에서는 가수부는 23비트로 정규화된 수의 소수점 23번째보다 아래에 있는 숫자는 저장할 수 없기 때문에 반올림하거나 특정한 로직에 따라 일관적(올림, 버림, 마지막 비트에 따라 다르게 등)으로 저장합니다. 따라서 해당 크기만큼의 오차가 발생하게 되며 이 오차가 프로그래밍 언어가 나타낼 수 있는 가장 작은 수보다 더 크다면 우리가 생각하는 값과 다르게 나옵니다. 십진수 0.1과 0.2는 모두 이진수로 변환하였을 때 순환소수가 되는 수이고 각각의 오차가 작지 않은 수이기 때문에 0.1 + 0.2 == 0.5false가 나오게 되는 것입니다.

크롬 개발자 도구의 결과 값

따라서 오차를 줄이고 싶다면 소수가 아닌 정수로 값을 바꾸어 저장(달러→센트, 킬로그램(kg)→그램(g) 등)하여 오차를 없애거나 64 비트 배정도를 이용하는 double(JAVA)과 같은 자료형을 이용하는 등 더 많은 비트를 이용하여 수를 저장하는 등의 방법이 있습니다. 참고로 자바스크립트의 숫자(number 자료)는 64비트의 배정밀도 형식으로 처리합니다.

참고 자료

모던 JavaScript 튜토리얼, 숫자형

위키백과, IEEE 754, 부동소수점

정보통신기술용어해설, IEEE 754

'CS(computer Science) > CS 공부' 카테고리의 다른 글

TCP/IP와 작동원리  (0) 2023.12.15
OSI 7계층(OSI 7Layer) 모델 핵심 정리  (0) 2023.12.14
캐시 (Cache)  (0) 2023.11.26
ERD(Entity Relationship Diagram, 개체 관계도)  (0) 2023.11.13

+ Recent posts