[JUNGLE] TIL_13. CSAPP 2.4

모깅·2025년 9월 17일

JUNGLE

목록 보기
14/56
post-thumbnail

2.4 부동소수점 (Floating Point)

부동소수점 표현법은 V=x×2yV=x×2^y 형태를 가지는 유리수를 인코딩하는 방식입니다. 이는 매우 큰 수, 0에 매우 가까운 수, 그리고 더 일반적으로는 실수 연산의 근사치를 다루는 데 매우 유용합니다.

1. 과거의 문제점: 통일되지 않은 표준

1980년대까지 모든 컴퓨터 제조사는 각자 자신만의 부동소수점 표현 방식과 연산 규칙을 사용했습니다. 또한, 수치적 정밀도보다는 속도와 구현의 용이성을 더 중요하게 여겨 연산의 정확성에 크게 신경 쓰지 않았습니다.

2. 해결책: IEEE 754 표준의 등장

이러한 모든 상황은 1985년경, 부동소수점 숫자와 그 연산을 표현하는 매우 정교한 표준인 IEEE 754가 등장하면서 바뀌었습니다.

이 표준화 작업은 1976년 인텔의 후원으로 시작되었으며, 윌리엄 카한(William Kahan) 교수가 인텔의 컨설턴트로 참여하여 미래 프로세서를 위한 부동소수점 표준을 설계했습니다. 이후 IEEE(국제 전기 전자 기술자 협회) 주관하에 산업계 표준을 만드는 위원회가 결성되었고, 이 위원회는 카한 교수가 제안한 표준을 채택했습니다.

오늘날에는 거의 모든 컴퓨터가 이 IEEE 부동소수점 표준을 지원하며, 덕분에 과학 응용 프로그램들이 서로 다른 컴퓨터 기종 사이에서도 동일하게 작동할 수 있게 되었습니다. (이식성 향상)

앞으로 다룰 내용

이 섹션에서는 IEEE 부동소수점 형식에서 숫자가 어떻게 표현되는지 살펴볼 것입니다. 또한, 숫자가 형식에 정확히 맞지 않아 올리거나 내려야 하는 반올림(rounding) 문제와, 덧셈, 곱셈, 비교 연산자의 수학적 속성에 대해서도 탐구할 것입니다. 많은 프로그래머들이 부동소수점을 어렵다고 생각하지만, IEEE 형식은 작고 일관된 원칙에 기반하고 있어 매우 우아하고 이해하기 쉽다는 것을 알게 될 것입니다.

2.4.1 소수 이진수 (Fractional Binary Numbers)

부동소수점을 이해하는 첫 단계는 소수점을 가진 이진수를 이해하는 것입니다.

1. 10진수 소수의 원리

우리가 익숙한 10진수 소수 12.34는 각 자릿수에 10의 거듭제곱 가중치를 곱하여 계산됩니다. 소수점을 기준으로 왼쪽은 양의 거듭제곱(100,101,...), 오른쪽은 음의 거듭제곱(10−1,10−2,...)을 사용합니다.

12.3410=1101+2100+3101+4102=12+310+410012.3410=1⋅10^1+2⋅10^0+3⋅10^{−1}+4⋅10^{−2}=12+{\frac 3 {10}}+{\frac 4 {100}}

2. 2진수 소수의 원리

2진수 소수도 같은 원리입니다. 각 자릿수에 2의 거듭제곱 가중치를 곱합니다. 소수점을 기준으로 왼쪽은 양의 거듭제곱(20,21,...), 오른쪽은 음의 거듭제곱(2−1,2−2,...)을 사용합니다.

101.112=122+021+120+121+122=4+0+1+12+14=534=5.7510101.112=1⋅2^2+0⋅2^1+1⋅2^0+1⋅2^{−1}+1⋅2^{−2}=4+0+1+{\frac 1 2}+{\frac 1 4}=5{\frac 3 4}=5.7510

  • 소수점 이동:
    • 2진 소수점을 왼쪽으로 한 칸 옮기면 숫자를 2로 나누는 효과가 있습니다.
    • 2진 소수점을 오른쪽으로 한 칸 옮기면 숫자를 2로 곱하는 효과가 있습니다.

3. 표현의 한계

유한한 자릿수의 10진수 소수로는 31(0.333...)을 정확히 표현할 수 없는 것처럼, 유한한 자릿수의 2진수 소수 역시 모든 수를 정확히 표현할 수는 없습니다.

2진수 소수는 오직 x×2yx×2^y 형태로 나타낼 수 있는 수만 정확하게 표현 가능합니다.

  • 예시: 51 (0.2)
    10진수로는 0.2로 정확히 표현되지만, 2진수로는 무한히 반복되는 소수가 되어 정확한 표현이 불가능합니다. 비트 수를 늘릴수록 근사치의 정확도만 높아질 뿐입니다.
    - 0.0011..._2
    - 0.00110011..._2
    - 0.001100110011..._2 = 0.19999...10

이러한 한계 때문에 컴퓨터는 많은 실수를 정확한 값이 아닌 가장 가까운 근사치로 표현하게 됩니다.

2.4.2 IEEE 부동소수점 표현법

단순한 2진 소수점 표기법은 매우 큰 수를 표현하기에 비효율적입니다. 대신 IEEE 부동소수점 표준은 숫자를 과학적 표기법과 유사한 V=(1)s×M×2EV=(−1)^s×M×2^E 형태로 표현합니다.

  • s (부호, Sign): 숫자가 양수(s=0)인지 음수(s=1)인지를 결정합니다.
  • M (가수, Significand): 실제 유효숫자를 나타내는 2진 소수입니다.
  • E (지수, Exponent): 2의 거듭제곱으로, 소수점의 위치를 옮기는 역할을 합니다.

이 세 요소를 인코딩하기 위해, 부동소수점 숫자의 비트 표현은 세 개의 필드로 나뉩니다.

타입전체 비트부호(s) 필드지수(exp) 필드가수(frac) 필드
float (단정밀도)321 비트8 비트23 비트
double (배정밀도)641 비트11 비트52 비트

'지수(exp)' 필드 값에 따른 3가지 해석 방식

컴퓨터는 지수(exp) 필드의 비트 패턴을 보고 숫자를 세 가지 다른 케이스로 해석합니다.

1. 정규화된 값 (Normalized Values) - 가장 일반적인 경우

  • 조건: 지수(exp) 필드가 모두 0도 아니고, 모두 1도 아닐 때입니다.
  • E (지수) 계산: E=e−Bias
    • e: 지수(exp) 필드를 부호 없는 정수로 읽은 값입니다.
    • Bias: 미리 정해진 값으로, float는 127, double은 1023입니다. 이 Bias 덕분에 음수 지수도 표현할 수 있습니다.
  • M (가수) 계산: M=1+f
    • f: 가수(frac) 필드를 0.frac 형태의 소수로 읽은 값입니다. (예: frac101이면 f는 0.101₂)
    • 항상 1이 맨 앞에 있다고 암묵적으로 가정하기 때문에, 23비트만으로 24비트의 정밀도를 얻는 효과가 있습니다.

2. 비정규화된 값 (Denormalized Values) - 0과 매우 작은 수

  • 조건: 지수(exp) 필드가 모두 0일 때입니다.
  • E (지수) 계산: E=1−Bias (가장 작은 지수 값으로 고정)
  • M (가수) 계산: M=f
    • 정규화된 값과 달리, 암묵적인 1이 없습니다. 가수(frac) 필드의 값이 그대로 가수가 됩니다.
  • 용도:
    1. 0을 표현: 가수(frac) 필드마저 모두 0이면, 이 숫자는 0이 됩니다. (s가 0이면 +0.0, 1이면 -0.0)
    2. 0에 매우 가까운 수 표현: 정규화 방식으로는 표현할 수 없는, 0에 아주 가까운 작은 숫자들을 표현하여 정밀도를 높입니다.

3. 특수 값 (Special Values) - 무한대와 NaN

  • 조건: 지수(exp) 필드가 모두 1일 때입니다.
  • 종류:
    • 무한대 (Infinity): 가수(frac) 필드가 모두 0일 때.
      • s가 0이면 양의 무한대(+∞), 1이면 음의 무한대()입니다.
      • 매우 큰 수를 곱하거나 0으로 나눌 때 결과로 나옵니다.
    • NaN (Not a Number): 가수(frac) 필드가 0이 아닐 때.
      • 숫자가 아님을 의미합니다.
      • 1\sqrt{-1}이나 ∞−∞ 와 같이 수학적으로 정의되지 않는 연산의 결과로 나옵니다.

2.4.3 부동소수점 예시

1. 부동소수점 수의 분포

IEEE 부동소수점 형식으로 표현할 수 있는 숫자들은 수직선 상에 균등하게 분포하지 않습니다. 대신 0에 가까울수록 더 촘촘하게(정밀하게) 분포합니다.

  • 비정규화된 수 (Denormalized): 0 주변에 매우 촘촘하게 모여 있어, 0에 아주 가까운 작은 값들을 정밀하게 표현합니다.
  • 정규화된 수 (Normalized): 0에서 멀어질수록 값들 사이의 간격이 점점 더 넓어집니다.
  • 무한대 (Infinity): 표현 가능한 범위를 벗어나는 양쪽 끝에 위치합니다.

비정규화된 수와 정규화된 수의 부드러운 전환

IEEE 표준은 비정규화된 수의 지수 E-Bias가 아닌 1-Bias로 정의하는 영리한 방법을 사용합니다. 이 덕분에 가장 큰 비정규화된 수와 가장 작은 정규화된 수 사이의 간격이 다른 숫자들 사이의 간격과 거의 비슷해져, 값들이 0 주변에서 갑자기 변하지 않고 부드럽게 이어지게 됩니다.

부드러운 전환의 의미 : 만약 이 규칙이 없었다면? (간격이 생기는 이유)

가상의 8비트 부동소수점(Bias=7)을 예로 들어보겠습니다.

가장 작은 정규화된 수 (정밀한 자의 첫 눈금)

  • 지수(exp) 필드: 가장 작은 값인 0001 (e=1)
  • 실제 지수 E = e - Bias = 1 - 7 = -6
  • 가수(frac) 필드: 가장 작은 값인 000
  • 가수 M = 1 + f = 1.0 (숨어있는 1 때문에)
  • 최종 값 = M × 2^E = 1.0 × 2⁻⁶2⁻⁶ = 1/64 (즉, 8/512)

가장 큰 비정규화된 수 (돋보기 자의 마지막 눈금) - 잘못된 규칙 적용

만약 비정규화된 수의 지수 E가 단순히 정규화된 수보다 하나 작은 -Bias(-7)였다고 가정해 봅시다.

  • 지수(exp) 필드: 0000
  • 실제 지수 E = -Bias = -7 (잘못된 가정)
  • 가수(frac) 필드: 가장 큰 값인 111
  • 가수 M = f = 0.111₂ = 7/8 (숨어있는 1이 없음)
  • 최종 값 = M × 2^E = (7/8) × 2⁻⁷2⁻⁷ = 7/1024

문제점: 가장 큰 비정규화된 수(7/1024)와 가장 작은 정규화된 수(8/512 = 16/1024) 사이에 엄청난 간격(gap)이 생깁니다. 숫자 체계가 뚝 끊어지는 것이죠.

→ IEEE의 영리한 해결책 (부드러운 전환)

IEEE 표준은 이 문제를 해결하기 위해 비정규화된 수의 지수 E정규화된 수의 가장 작은 지수와 똑같이 맞춰주는 방법을 사용합니다.

가장 큰 비정규화된 수 - 올바른 규칙 적용

  • 지수(exp) 필드: 0000
  • 실제 지수 E = 1 - Bias = 1 - 7 = -6 (규칙 적용!)
  • 가수(frac) 필드: 111
  • 가수 M = f = 0.111₂ = 7/8
  • 최종 값 = M × 2^E = (7/8) × 2⁻⁶ = 7/512

결과:

  • 가장 큰 비정규화된 수: 7/512
  • 가장 작은 정규화된 수: 8/512

보시는 것처럼, 두 숫자 사이의 간격이 1/512로, 다른 숫자들 사이의 간격과 일정하게 유지됩니다. 마치 두 개의 자가 아무런 빈틈없이 완벽하게 이어지는 것과 같습니다.


2. 비트 표현과 정수 정렬

IEEE 형식의 흥미로운 특징 중 하나는, 양수 부동소수점 값들의 비트 패턴을 부호 없는 정수로 해석하면 그 크기 순서가 실제 부동소수점 값의 크기 순서와 일치한다는 점입니다.

  • 예시 (8비트 가상 형식):
    • 00000 0000
    • 1/5120000 0001
    • ...
    • 240 (최대 정규화 수) → 0111 0111
    • +∞0111 1000

이처럼 비트 값의 크기와 실제 수의 크기가 비례하도록 설계되었기 때문에, 일반적인 정수 정렬 알고리즘을 사용해서 부동소수점 숫자를 정렬할 수 있어 하드웨어적으로 매우 효율적입니다.


3. 정수를 부동소수점으로 변환하기

정수 12,345를 32비트 float으로 변환하는 과정은 다음과 같습니다.

  1. 2진수로 변환:
    1234511000000111001₂
  2. 정규화: 1.xxxx... 형태로 만듭니다. 소수점을 왼쪽으로 13칸 이동해야 합니다.
    1.1000000111001₂ × 2¹³
  3. 필드 값 결정:
    • 부호(s): 양수이므로 0.
    • 실제 지수(E): 13.
    • 가수(frac) 필드: 가수 1.1000000111001에서 1.을 제외한 1000000111001을 저장하고, 23비트를 채우기 위해 뒤에 0을 10개 붙입니다.
    • 지수(exp) 필드: e = E + Bias = 13 + 127 = 140. 140의 8비트 2진수는 10001100.
  4. 최종 조립:
    s | exp | frac0 | 10001100 | 10000001110010000000000

이 32비트 패턴을 16진수로 변환하면 0x4640E400이 됩니다.

2.4.4 근사치 (Rounding)

부동소수점은 표현의 정밀도에 한계가 있기 때문에 실제 값을 완벽하게 나타낼 수 없고, 가장 가까운 값으로 근사해야 합니다. 이 과정을 반올림(Rounding)이라고 합니다.

IEEE 부동소수점 표준은 네 가지의 서로 다른 반올림 모드를 정의합니다.

1. 짝수쪽으로 반올림 (Round-to-even) - 기본값

'가장 가까운 짝수 값으로' 반올림하는 방식입니다.

  • 일반적인 경우: 두 대표값 중 더 가까운 쪽으로 반올림합니다. (예: 1.401, 1.602)
  • 정확히 중간 값일 경우 (Tie-breaking): 두 대표값(1 또는 2) 중 마지막 자리 숫자가 짝수인 쪽으로 반올림합니다.
    • 1.5012의 중간. 2가 짝수이므로 2로 반올림.
    • 2.5023의 중간. 2가 짝수이므로 2로 반올림.

왜 짝수 쪽으로 반올림할까요? (통계적 편향 방지)

만약 중간 값을 항상 올림(우리가 흔히 쓰는 '사사오입')한다면, 많은 데이터를 처리했을 때 전체 평균값이 실제보다 약간 더 커지는 통계적 편향이 발생합니다. 반대로 항상 내림하면 평균값이 더 작아지겠죠.
'짝수쪽으로 반올림'은 중간 값을 올리는 경우와 내리는 경우의 확률을 거의 50:50으로 만들어, 이러한 통계적 편향을 최소화하는 매우 합리적인 방법입니다.

2. 0쪽으로 반올림 (Round-toward-zero)

양수는 내리고 음수는 올립니다. 즉, 소수점 이하를 그냥 버리는(truncate) 것과 같습니다.

  • 1.81
  • -1.8-1

3. 아래로 반올림 (Round-down)

항상 더 작은 값(음의 무한대 방향)으로 내립니다. 수학의 floor() 함수와 같습니다.

  • -1.8-1
  • 1.22

4. 위로 반올림 (Round-up)

항상 더 큰 값(양의 무한대 방향)으로 올립니다. 수학의 ceil() 함수와 같습니다.

  • -1.2-2
  • 1.81

2.4.5 부동소수점 연산 (Floating-Point Operations)

IEEE 표준은 덧셈이나 곱셈 같은 산술 연산의 결과를 결정하는 간단한 규칙을 명시합니다. 그 규칙은 "두 실수를 가지고 정확한 연산을 수행한 뒤, 그 결과를 표준 반올림 규칙에 따라 처리한 값"을 최종 결과로 삼는 것입니다.

1. 부동소수점 덧셈

부동소수점 덧셈(x +f y)은 우리가 아는 덧셈의 여러 수학적 속성을 따르지만, 반올림오버플로우 때문에 중요한 차이점을 보입니다.

  • 교환 법칙 (Commutative): x + y = y + x는 성립합니다.
  • 결합 법칙 (Associative): 성립하지 않습니다.
    • 이유: 매우 큰 수와 매우 작은 수를 더할 때, 작은 수가 정밀도 한계로 인해 무시(rounding)될 수 있기 때문입니다.
    • 예시: (3.14 + 1e10) - 1e10
      1. 3.14 + 1e10을 계산하면, 3.14는 너무 작은 값이어서 무시되고 결과는 1e10이 됩니다.
      2. 1e10 - 1e10의 최종 결과는 0.0이 됩니다.
    • 예시 2: 3.14 + (1e10 - 1e10)
      1. 1e10 - 1e100.0입니다.
      2. 3.14 + 0.0의 최종 결과는 3.14입니다.

이처럼 계산 순서에 따라 결과가 달라질 수 있으므로, 컴파일러는 부동소수점 연산의 순서를 함부로 바꾸는 최적화를 매우 보수적으로 수행합니다.

2. 부동소수점 곱셈

부동소수점 곱셈(x *f y)도 덧셈과 유사한 특징을 가집니다.

  • 교환 법칙: x * y = y * x는 성립합니다.
  • 결합 법칙: 성립하지 않습니다. (오버플로우 또는 정밀도 손실 때문)
    • 예시: (1e20 * 1e20) * 1e-20∞ * 1e-20+∞
    • 예시 2: 1e20 * (1e20 * 1e-20)1e20 * 1.01e20
  • 분배 법칙: a * (b + c) = a * b + a * c성립하지 않습니다.

3. 단조성 (Monotonicity)

정수 연산과 달리, 부동소수점 연산은 단조성이라는 중요한 속성을 만족합니다. (NaN 제외)

  • 덧셈: a ≥ b 이면, x + a ≥ x + b 입니다.
  • 곱셈: a ≥ b이고 c ≥ 0이면, a * c ≥ b * c 입니다.

이러한 비직관적인 특징들(특히 결합법칙의 부재)은 과학 계산 프로그래머나 컴파일러 개발자에게 매우 중요한 고려사항이며, 정확한 수치 계산을 어렵게 만드는 원인이 됩니다.

2.4.6 C언어에서의 부동소수점

모든 C언어 버전은 floatdouble이라는 두 가지 부동소수점 자료형을 제공합니다.

IEEE 부동소수점 표준을 지원하는 컴퓨터에서는 이 두 자료형이 각각 단정밀도(single-precision)배정밀도(double-precision)에 해당하며, 기본 반올림 방식으로는 '짝수쪽으로 반올림(round-to-even)'을 사용합니다. 하지만 C 표준 자체가 IEEE 방식을 강제하지는 않기 때문에, 반올림 모드를 바꾸거나 +∞, NaN 같은 특수 값을 사용하는 표준화된 방법은 없습니다. (대부분 시스템 라이브러리를 통해 개별적으로 지원합니다.)

자료형 간의 형 변환 규칙

int, float, double 사이에서 형 변환이 일어날 때, 숫자 값과 비트 표현은 다음과 같이 변합니다. (int가 32비트라고 가정)

  • intfloat:
    int의 모든 값을 표현할 만큼 float의 범위는 충분히 넓어서 오버플로우는 발생하지 않습니다. 하지만 float의 정밀도(유효 자릿수)는 약 7자리이므로, 그보다 큰 정수는 반올림되어 근사값으로 저장될 수 있습니다.
    - float의 지수부는 -128 ~ 127을 표현 할 수 있습니다. 즉, 21272^{127}까지의 정수를 표현 할 수 있으므로 float의 범위가 더 넓기 때문에 int 담아도 오버플로우가 발생하지 않습니다.
    - float의 정밀도가 약 7자리인 이유는 가수부가 23비트이며 정수 부분의 1을 생략하기 때문에 1비트를 추가로 계산할 수 있습니다. 즉, 2242^{24}만큼 표현할 수 있으며 이는 약 10710^7과 비슷합니다. 따라서 7자리까지 유효 자릿수로 표현한다고 할 수 있습니다.
  • int 또는 floatdouble:
    double은 범위와 정밀도가 훨씬 더 뛰어나므로, 정확한 숫자 값이 그대로 보존됩니다.
  • doublefloat:
    double이 표현하던 수가 float의 표현 범위를 벗어나면 +∞ 또는 로 오버플로우가 발생할 수 있습니다. 범위 안에 있더라도, 정밀도가 더 낮기 때문에 반올림될 수 있습니다.
  • float 또는 doubleint:
    소수점 이하는 0을 향해 버려집니다(truncate).
    - 1.9991
    - 1.9991
    또한, 실수가 int의 표현 범위를 벗어나면 오버플로우가 발생할 수 있습니다. C 표준은 이때의 결과를 정해두지 않았지만, 인텔 호환 프로세서는 이런 경우 정수(integer indefinite) 값, 즉 가장 작은 음수(TMin, -2147483648)를 결과로 내놓습니다. (예: (int) +1e102147483648)

2.5 요약 (Summary)

컴퓨터는 정보를 비트(bit)로 인코딩하며, 이 비트들은 일반적으로 바이트(byte) 단위로 묶여 관리됩니다. 정수, 실수, 문자열을 표현하기 위해 서로 다른 인코딩 방식이 사용되며, 컴퓨터 기종마다 숫자 인코딩 방식이나 바이트 순서(엔디언)에 대한 규칙이 다릅니다.

C언어와 데이터 표현

C언어는 다양한 워드 크기와 숫자 인코딩 방식을 수용하도록 설계되었습니다. 최근 32비트 머신을 대체하여 64비트 워드 크기를 가진 머신이 보편화되었습니다. 64비트 머신은 32비트용으로 컴파일된 프로그램도 실행할 수 있으므로, 기계 자체보다는 32비트 프로그램과 64비트 프로그램의 차이에 초점을 맞추는 것이 중요합니다. 64비트 프로그램의 가장 큰 장점은 32비트의 4GB 주소 공간 한계를 넘어설 수 있다는 점입니다.

대부분의 컴퓨터는 부호 있는 숫자를 2의 보수 방식으로, 부동소수점 숫자는 IEEE 754 표준으로 인코딩합니다. 프로그래머가 모든 숫자 범위에서 올바르게 동작하는 프로그램을 작성하기 위해서는 이러한 인코딩 방식을 비트 수준에서 이해하고 산술 연산의 수학적 특징을 파악하는 것이 중요합니다.

정수 연산의 특징

  • 형 변환: 같은 크기의 부호 있는 정수와 부호 없는 정수 사이의 형 변환은 기저의 비트 패턴을 그대로 유지한 채 해석 방식만 바꿉니다. 이 때문에 C언어의 암묵적 형 변환은 프로그래머가 예상치 못한 결과를 낳아 버그의 원인이 될 수 있습니다.
  • 유한성: 컴퓨터의 정수 연산은 유한한 비트 길이 때문에 실제 정수 연산과 다른 속성을 가집니다. 표현 가능한 범위를 넘어서면 오버플로우(overflow)가 발생할 수 있으며, 이로 인해 x*x가 음수가 되는 등 이상한 결과가 나올 수 있습니다.
  • 최적화: 그럼에도 불구하고 컴퓨터의 정수 연산은 교환, 결합, 분배 법칙과 같은 여러 속성을 만족하므로 컴파일러가 다양한 최적화를 수행할 수 있습니다. (예: 7*x(x<<3)-x로 변환)

부동소수점 연산의 특징

  • 표현: 부동소수점은 x×2yx×2^y 형태의 수를 인코딩하여 실수의 근사값을 표현합니다. IEEE 754 표준이 가장 널리 쓰이며, 단정밀도(32비트)와 배정밀도(64비트)가 일반적입니다. 또한, 무한대(infinity)나 숫자가 아님(NaN)을 나타내는 특수 값도 표현할 수 있습니다.
  • 주의점: 부동소수점 연산은 표현 가능한 범위와 정밀도가 제한적이고, 결합 법칙과 같은 일반적인 수학적 속성을 따르지 않기 때문에 매우 신중하게 사용해야 합니다.
profile
멈추지 않기

0개의 댓글