본문 바로가기

소수·부동소수점·문자

이진 소수와 정규화·반올림을 계산하고 문자 코드·UTF-8을 값의 해석으로 연결합니다.

0.1+0.2가 화면에서 0.3과 약간 다르게 보일 수 있습니다. 비트 수가 유한하면 모든 실수를 정확히 저장할 수 없기 때문입니다. 또 같은 바이트를 숫자로 읽을지 문자로 읽을지도 규약에 달려 있습니다.

이진 소수와 정규화

이진 소수의 소수점 아래 자리값은 1/2·1/4·1/8입니다. 0.101₂ = 1/2 + 1/8 = 0.625입니다. 13.625는 1101.101₂이고, 소수점을 옮기면 1.101101₂ × 2³입니다. 소수점을 정해진 형태로 옮기고 지수로 위치를 기록하는 것을 정규화라고 합니다.

고정소수점은 소수점 위치를 미리 정합니다. 예를 들어 하위 2비트를 분수로 읽으면 저장 정수 25는 25/4=6.25입니다. 부동소수점은 부호·지수·유효숫자를 나누어 넓은 범위를 표현합니다. 넓은 범위가 모든 값의 정밀도를 보장하지는 않습니다.

binary32를 한 값으로 읽기

IEEE 754의 binary32 형식은 부호 1비트·지수 8비트·분수 23비트입니다. 정상값의 실제 지수는 저장 지수-127이고, 유효숫자는 앞의 1을 포함한 1.분수입니다. 13.625는 부호 0, 저장 지수 130=10000010, 분수 101101 뒤에 0을 채운 패턴입니다.

지수 필드가 모두 0 또는 모두 1이면 이 정상값 식을 그대로 사용하지 않습니다. 0·비정규값(subnormal), 무한대, NaN에 별도 규칙이 있습니다. 언더플로우를 “작은 값이면 무조건 오류로 종료”라고 이해하지 마세요. 형식과 연산의 결과 처리 규칙을 함께 봐야 합니다.

덧셈은 지수를 맞춘 후 유효숫자를 더하고 정규화·반올림합니다. 곱셈은 유효숫자를 곱하고 실제 지수를 더한 후 정규화합니다. 표현할 수 있는 자리가 부족하면 끝자리 정보를 잃습니다. 덧셈의 결합법칙이 실제 부동소수점에서 그대로 성립한다고 가정하지 않습니다.

import math
import struct

packed = struct.pack(">f", 13.625)
bits = int.from_bytes(packed, "big")
print(f"{bits:032b}")
assert (bits >> 31) == 0
assert ((bits >> 23) & 0xff) == 130
assert struct.unpack(">f", packed)[0] == 13.625
assert 0.1 + 0.2 != 0.3
assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-12)

struct는 바이트 형식을 지정하고, >f는 빅 엔디언 binary32를 뜻합니다. Python의 일반 float 연산과 binary32 형식으로 바이트를 만드는 작업은 구분합니다. 출력은 01000001010110100000000000000000입니다. 근삿값을 비교할 허용 오차는 문제의 단위와 요구 정확도에서 결정해야 합니다.

반올림의 중간 과정을 계산하기

binary32 정상값은 숨겨진 앞자리 1과 분수 23비트로 유효숫자 24비트를 가집니다. 2²⁴부터 2²⁵ 미만에서 이웃한 표현의 간격은 2²⁴⁻²³=2입니다. 따라서 2²⁴=16,777,216과 그다음 16,777,218은 표현할 수 있지만, 중간 정수 16,777,217은 정확히 표현하지 못합니다.

여기서는 최근접·동률이면 짝수(round to nearest, ties to even) 규칙을 씁니다. 가까운 표현을 고르고, 두 표현까지의 거리가 같으면 유효숫자의 마지막 비트가 0인 표현을 고릅니다. 후보 정수 둘이 모두 짝수일 수 있으므로 단순히 정수 값의 홀짝을 검사하는 규칙은 아닙니다.

16,777,217은 16,777,216과 16,777,218에서 각각 거리 1입니다. 첫 후보의 마지막 유효 비트가 0이므로 16,777,216으로 갑니다. 16,777,219의 후보는 16,777,218과 16,777,220이며 두 번째 후보의 마지막 유효 비트가 0이므로 16,777,220을 고릅니다. 항상 아래로 버리는 절삭과 다릅니다.

import struct

def binary32(value):
    packed = struct.pack(">f", value)
    return struct.unpack(">f", packed)[0], int.from_bytes(packed, "big")

assert binary32(2**24 + 1) == (float(2**24), 0x4b800000)
assert binary32(2**24 + 3) == (float(2**24 + 4), 0x4b800002)
assert binary32(2**24 + 2)[0] == 2**24 + 2

이 실행은 Python 정수에서 binary32로 변환할 때의 위 규칙을 확인합니다. 다른 반올림 모드를 지정한 계산은 그 모드로 다시 판단해야 합니다. 결과 비트의 끝자리도 확인하면 후보의 의미를 값과 함께 볼 수 있습니다.

문자도 해석 규약입니다

ASCII는 7비트 코드로 영문자·숫자·제어문자를 표현합니다. 문자 '7'의 ASCII 코드 55는 정수 값 7이 아닙니다. Unicode는 문자를 코드 포인트로 식별하고, UTF-8은 그 코드 포인트를 1~4바이트로 인코딩하는 방식입니다. 코드 포인트 하나가 사용자 눈의 글자 하나와 항상 일치하지도 않습니다.

BCD(Binary Coded Decimal)는 현대적으로는 십진수 각 자리를 4비트로 표현하는 문맥이 흔합니다. 59는 0101 1001이므로 부호 없는 이진 정수 59의 0011 1011과 다릅니다. 역사적 6비트 문자 코드인 BCD 계열·EBCDIC와 이 십진 숫자 표현을 같은 규칙으로 계산하지 않습니다. 과거 코드의 목록보다 “어떤 규약을 썼는가”를 확인하는 습관이 중요합니다.

assert ord("7") == 55
assert "가".encode("utf-8").hex() == "eab080"
assert len("가".encode("utf-8")) == 3
assert bytes.fromhex("eab080").decode("utf-8") == "가"

독립 문제: 같은 비트를 다르게 읽기

0.011₂의 값을 구하세요. 고정소수점에서 소수 4비트를 쓰면 정수 패턴 18의 실제 값은 얼마인가요? ASCII 바이트 0x41을 부호 없는 정수·문자로 각각 해석하고, “한글 한 글자는 항상 2바이트”라는 주장을 평가하세요.

힌트

분수의 첫 자리는 1/2입니다. 고정소수점의 분모는 2⁴입니다. 문자의 길이는 인코딩을 명시해야 합니다.

풀이

0.011₂는 1/4+1/8=0.375입니다. 패턴 18은 18/16=1.125입니다. 0x41은 정수 65이며 ASCII 문자 A입니다. UTF-8의 가는 3바이트이므로 2바이트 주장은 일반적으로 맞지 않습니다. 문자 수·코드 포인트 수·바이트 수는 다른 측정입니다.

반올림 독립 문제

같은 binary32 구간·최근접 동률 짝수 규칙에서 2²⁴+5는 어느 값으로 변환될까요? 이웃 표현 두 개, 거리, 고르는 이유를 쓰세요.

힌트: 후보는 2²⁴+4와 2²⁴+6입니다. 비트 패턴은 각각 0x4b800002, 0x4b800003입니다.

풀이: 두 후보까지 거리가 각각 1이어서 동률입니다. 마지막 유효 비트가 0인 0x4b800002를 골라 2²⁴+4가 됩니다. 동률 선택은 유효숫자 마지막 비트를 기준으로 하며, 무조건 큰 값이나 무조건 작은 값을 고르지 않습니다.

공식 형식은 IEEE 754 표준 안내와 Unicode FAQ를 참고하세요. 다음 게이트에서는 이 비트들을 실제로 계산하는 논리 규칙을 배웁니다.