# 자료형(Type)에 따른 오차 발생 사례

#### 컴퓨터 내부에서의 수 표현

디지털 컴퓨터는 유한한 비트(bit)의 조합으로 실수를 표현한다. 이때 여러 가지 자료형이 제공되지만, 각각 표현할 수 있는 값의 범위와 정밀도가 다르다. 일반적으로 정수형(integer type)은 유리수를 정수 단위로, 부동소수점형(floating-point type)은 실수를 가법적으로 근사하여 표현한다. 이러한 자료형들은 모두 컴퓨터 메모리 상에 비트로 저장되므로, 실제 연산 과정에서 근사 오차가 발생하거나 범위를 벗어나서 오버플로우나 언더플로우가 발생한다.

정수형의 경우에는 모든 소수가 잘려 나가며, 부동소수점형에서는 가장 하위 비트에서 반올림이 일어난다. 이로 인해, 이론적인 수학 계산 결과와 실제 컴퓨터 연산 결과 사이에 오차가 생긴다. 자료형에 따라 허용하는 최대값과 최소값, 그리고 표현 가능한 유효 숫자 자릿수가 다르기 때문에 이를 염두에 두지 않고 계산하면 예기치 못한 결과를 얻는다.

#### 정수형에서의 오차 발생

정수형 자료형은 메모리 사용량에 따라 표현할 수 있는 정수 범위가 결정된다. 예를 들어, 어떤 시스템에서 32비트 정수형을 사용할 때 표현할 수 있는 범위는 대체로 $-2^{31}$부터 $2^{31} - 1$ 사이이다. 이 범위를 초과하는 값으로 연산을 시도하면 오버플로우가 발생한다. 예를 들어, $2,147,483,647 + 1$을 32비트 정수에서 계산하면 음수 값으로 래핑(wrapping)되어 버릴 수 있다.

언더플로우도 유사한 방식으로 발생한다. 단지 정수형에서는 0 이하의 값이 표현되더라도 부호 비트를 포함해 오버플로우처럼 처리될 뿐, 부동소수점에서의 언더플로우와는 거리가 있다. 다만 절댓값이 매우 큰 음수를 처리할 때에도 오버플로우가 일어날 수 있다.

정수 연산에서는 소수점 이하를 고려하지 않으므로, 나눗셈이나 평균 계산 등에서 극명하게 정밀도 손실이 드러난다. 예를 들어, $5 / 2$를 정수형으로 나누면 $2.5$가 아닌 $2$로 계산된다. 이 때문에 부동소수점 자료형을 써야 할 곳에서 정수형 자료형을 쓰면 큰 오차가 발생하거나, 혹은 일부 알고리즘에서는 스케일링을 통해 소수점 연산을 흉내내기도 한다.

#### 부동소수점 표현의 원리

부동소수점형은 실수를 근사 표현하기 위해 부호(sign), 유효숫자부(significand or mantissa), 지수(exponent)의 조합으로 수를 나타낸다. IEEE 754 표준은 이러한 방식을 규정하고 있으며, 단정밀도(single precision)와 배정밀도(double precision) 등의 형태가 널리 쓰인다.

부동소수점 수 $x$를 실제로 저장할 때는 대략 아래와 같은 꼴로 표현한다고 볼 수 있다.

$$
x = (-1)^{s} \times 1.\dots m \times 2^{e - \text{bias}}
$$

여기서 $s$는 부호 비트이고, $m$은 유효숫자부, $e$는 지수부이며, $\text{bias}$는 지수부를 양수로 맞춰주기 위한 상수이다. 예를 들어 단정밀도에서는 지수부가 8비트이므로 $\text{bias} = 127$이다. 따라서 단정밀도 부동소수점에서 지수 $e$가 $127$이라면 실제 지수는 $e - \text{bias} = 0$이다.

이처럼 고정된 비트 수 안에 들어가는 부동소수점 형태로 실수를 표현하므로, 컴퓨터에서 다룰 때는 근사값을 사용한다. 10진수로 딱 떨어지는 값이라도, 2진 부동소수점 표현이 유한 길이를 가지지 못하면 무한소수가 되어 반올림이 필연적으로 생긴다. 예를 들어, $0.1\_{10}$은 2진수로 유한 길이로 나타낼 수 없으므로 내부적으로는 $0.0,1100,1100,1100,\dots\_{2}$ 같은 형태가 된다.

#### 부동소수점에서의 반올림오차

부동소수점 연산을 할 때 발생하는 가장 대표적인 오차는 반올림오차(rounding error)이다. 예를 들어, $0.1$과 $0.2$를 부동소수점으로 더한 결과가 정확히 $0.3$이 아니라 $0.30000000000000004$ 같은 값이 될 수 있다. 내부 표현을 살펴보면, $0.1$, $0.2$, $0.3$ 모두 2진 표현으로 무한소수가 되어 잘려 나가므로 미세한 차이가 누적된다.

컴퓨터는 일정한 자리수만 표현할 수 있기에, 최종 결과를 저장할 때 마지막 비트에서 반올림을 수행한다. 이 반올림 규칙은 IEEE 754에서 기본적으로 ‘Round to Nearest, Ties to Even’을 쓴다. 이 때문에 같은 표현이라도 계산 과정 중에 $\pm 1$ 차이가 생길 수 있으며, 누적되면 점차 눈에 띄는 오차로 확대되기도 한다.

특히 연산 과정에서 작은 수와 큰 수를 더하거나 뺄 때 상대적으로 정밀도가 급격히 손실되는 사례가 발생한다. 이를 ‘수치적 취약성(numerical instability)’ 또는 ‘catastrophic cancellation’이라고 한다. 예를 들어, 매우 큰 수 $a$와 $a$에 비해 상대적으로 매우 작은 수 $b$를 더하는 과정에서 $b$가 반올림되어 사라지는 문제가 생긴다.

단순 예시로 $a = 10^{6}$, $b = 10^{-2}$라 하면,

$$
a + b \approx a
$$

로 계산되어 $b$가 유효숫자부에서 반올림되어버릴 가능성이 있다. 알고리즘 설계 단계에서 이러한 성질을 고려하지 않으면, 누적된 반올림오차가 결과를 왜곡시킨다.

#### 단정밀도와 배정밀도

부동소수점에서 가장 흔히 쓰이는 것은 단정밀도(single precision)와 배정밀도(double precision)이다. 단정밀도는 32비트, 배정밀도는 64비트로 표현한다. 단정밀도에서 유효숫자부가 약 23비트, 배정밀도에서 유효숫자부가 약 52비트이므로, 배정밀도가 훨씬 더 작은 값을 다룰 수 있고 정밀도도 높다. 예를 들어 단정밀도는 대략 $10^{-7}$ 수준의 정밀도를 가지는 반면, 배정밀도는 $10^{-16}$ 정도까지 표현 가능하다.

다만 단정밀도를 쓰면 메모리 사용량과 연산 속도 면에서 장점이 있을 수 있지만, 오차가 누적되는 알고리즘에서는 배정밀도가 필수일 때가 많다. 예컨대 수열의 합이나 누적 곱을 취할 때, 단정밀도를 쓰면 반올림오차가 금방 눈에 띌 정도로 커질 수 있다. 반면 배정밀도는 연산 과정에서 반올림오차가 천천히 누적되므로, 더 안전한 결과를 얻을 가능성이 높다.

아래는 간단한 예제 코드로, 부동소수점 연산에서 오차가 누적되는 모습을 보여준다. Python에서 단정밀도를 강제하려면 패키지 활용 등이 필요하므로, 일단 기본 double precision 상태로 예시를 보인다.

```python
x = 0.0
for i in range(1000000):
    x += 0.000001
print(x)
```

단순히 0.000001을 백만 번 더했으니 이론상 $1.0$이 되어야 하지만, 실제 출력값은 $0.999999999999$ 근처나 다른 수치가 될 수 있다. 이런 미세한 차이는 2진 부동소수점에서 $0.000001\_{10}$을 나타내지 못해 발생하는 반올림오차로 누적된 결과이다.

#### 고정소수점과 임의정밀도

부동소수점 자료형이 널리 쓰이지만, 일부 응용 분야에서는 고정소수점(fixed-point) 방식이나 임의정밀도(arbitrary precision) 방식을 활용하기도 한다. 고정소수점 방식은 소수점을 기준으로 앞뒤로 몇 비트를 할당할지 미리 결정해 둔다. 예컨대 정수부 16비트, 소수부 16비트를 할당하면, 실제로는 32비트 공간을 모두 정수처럼 다루되, 소수부가 16비트라는 사실을 연산 과정에서 해석하여 소수 연산을 흉내 내는 식이다. 이 방식은 특정 응용에서 반복곱이나 합산 시 오차가 부동소수점 방식보다 예측 가능하게 축적되므로, 실시간 임베디드 시스템이나 DSP(digital signal processing) 분야에서 사용되는 경우가 있다.

그러나 고정소수점 방식은 표현 범위가 매우 제한적이다. 예컨대 소수부 비트를 16개 두면, 표현 가능한 소수 부분의 정밀도는 $2^{-16}$에 해당한다. 게다가 큰 정수가 등장할 경우 오버플로우 여지가 있기 때문에, 설계 단계에서 실제로 다뤄야 할 값들의 범위와 정밀도를 사전에 잘 파악해야 한다. 정밀도가 필요 없고 범위가 일정한 시스템 설계에서는 고정소수점이 오히려 계산 속도와 메모리 측면에서 유리하다.

임의정밀도 방식은 사용자가 원하는 만큼 정밀도를 늘려가며 수를 다루는 기법이다. 예컨대 Python의 decimal이나 mpmath, C++의 GMP 라이브러리 같은 것이 이에 속한다. 이들은 내부적으로 정수 배열이나 특정 구조체를 이용해, 사실상 제한 없는 정밀도의 실수를 표현할 수 있다. 심지어 어떤 라이브러리는 유리수(rational number)를 분자 분모 형태로 보관하여 완전히 정확한 연산 결과를 구현할 수 있도록 한다. 물론 이 방법은 연산 속도가 느려지며, 메모리 사용량도 크게 늘어난다.

현실적인 수치해석 문제에서 임의정밀도가 반드시 필요한 경우는 그리 많지 않지만, 매우 민감한 계산을 다룰 때는 필수적일 수 있다. 예를 들어 다항식 근을 매우 정밀하게 구해야 하거나, 문제 해의 안정성을 엄밀하게 검증해야 할 때 활용한다. 고정소수점과 임의정밀도는 모두 특수한 상황에서 사용되며, 부동소수점 연산의 한계를 인지하고 이를 보완하려는 목적에서 개발된 방법론이다.

#### 혼합정밀도(Mixed precision) 사용 시 발생하는 오차

혼합정밀도 기법은 메모리나 연산 시간 등을 최적화하기 위해, 계산 과정 중 일부는 단정밀도로 수행하고, 결과에 중요한 영향을 주는 연산만 배정밀도를 쓰는 식의 절충안이다. 예를 들어, 대규모 행렬 연산을 수행하는 알고리즘에서, 중간 단계는 단정밀도로 계산해도 결과 정확도에 큰 영향을 주지 않는다면 해당 단계에서 계산 속도를 높이고 메모리 사용을 줄일 수 있다. 다만, 이런 전략을 잘못 적용하면 반올림오차가 누적되어 결과 신뢰도가 크게 떨어질 수 있다.

혼합정밀도 전략은 특히 GPU에서 딥러닝 연산을 최적화할 때 널리 알려졌다. 예컨대 학습 초기 단계에서는 단정밀도나 반정밀도(half precision) 연산을 사용해 빠르게 움직이되, 안정화가 필요한 후반부 혹은 예측 단계에서는 배정밀도를 활용해 정밀한 업데이트를 진행한다. 그러나 이런 혼합정밀도 활용에서 오차가 어떻게 누적되고 최종 성능에 어느 정도 영향을 미치는지 세심한 분석이 필요하다. 수학적으로는 적분과 미분 연산 시, 오차항의 크기와 형태가 정밀도에 따라 달라질 수 있으며, 이를 적절히 추적하려면 에러 전파 분석(Forward Error Analysis or Backward Error Analysis)을 정교하게 적용해야 한다.

혼합정밀도를 적용할 때는 아래와 같은 함의가 있다. 첫째, 중간 계산 결과가 충분히 작은 상대 오차만을 허용한다면, 단정밀도로 연산해도 최종 결과는 원하는 수준의 정밀도를 유지할 수 있다. 둘째, 전역적으로는 배정밀도로 다시 한번 보정해 주는 과정을 두면, 중간 단계에서 누적된 오차를 어느 정도 완화할 수 있다. 그러나 이러한 과정에서 추가 연산이 필요한지, 속도 이점이 얼마나 남는지 등에 대한 사전 검증이 필수다.

아래는 C++ 예시 코드로, 혼합정밀도를 흉내내는 매우 단순화된 예이다. 첫 번째 절에서는 float 형을 이용해 간단한 수열 합산을 한다. 그 다음 double 형을 이용해 후처리를 통해 한번 더 보정하는 과정을 시뮬레이션한다.

```c++
C++
#include <iostream>
#include <cmath>
using namespace std;

int main() {
    float sumFloat = 0.0f;
    for (int i = 0; i < 1000000; i++) {
        sumFloat += 0.000001f;
    }
    double corrected = (double)sumFloat;

    // 배정밀도에서 후처리 연산(단순 예시)
    corrected = corrected + (1.0 - corrected) * 0.1;

    cout.precision(16);
    cout << "Mixed precision result: " << corrected << endl;

    return 0;
}
```

위의 코드는 실무적으로는 아무런 의미가 없지만, 예시처럼 중간 단계에서 float를 쓰고 나중에 double로 약간의 수정(corrected)을 한다는 개념적 흐름만 보여준다. 실제 연구나 실무에서는 혼합정밀도를 다룰 때, 알고리즘 전반에서 생길 수 있는 오차 누적과 그 파급 효과를 치밀하게 점검해야 한다.

#### IEEE 754 특별값(NaN, Infinity)

IEEE 754 부동소수점 표준에서는 특수한 값을 정의하여, 특정 오류 상황이나 극한 상황을 나타낸다. 분모가 0인 나눗셈 결과나 오버플로우 등으로 $+\infty$ 또는 $-\infty$가 발생할 수 있으며, 정의되지 않은 연산이나 0으로 0을 나누는 등의 경우에는 NaN(Not a Number) 값이 생긴다. 실수 연산에서 NaN은 전염성이 높아서, 어떤 계산 과정에서 NaN이 한 번 발생하면 그 뒤로 연산 결과가 계속 NaN이 될 가능성이 크다.

실수 범위 바깥으로 값이 튀면, 컴퓨터는 $\pm \infty$로 표시하곤 한다. 예를 들어 부동소수점 단정밀도에서 표현할 수 있는 최대값을 초과하면 곧바로 무한대 값을 반환한다. 무한대에 어떤 유한한 값을 더하거나 곱해도 여전히 무한대가 되므로, 이후 연산에도 영향을 미친다. 이런 상황은 수치해석 알고리즘에서 실패를 의미할 수 있으므로, 알고리즘 레벨에서 예외 처리를 하거나 로깅(logging)을 통해 디버깅해야 한다.

NaN은 그보다 더 특별한 비트 패턴으로 표현된다. NaN을 유도하는 사례로는 $0/0$ 같은 명백히 정의되지 않은 연산, 혹은 NaN 자체와의 연산이 대표적이다. NaN은 부호가 없고 확장 구간을 표시하는 일종의 예외 코드 같은 개념이다. 수치해석 코드를 작성할 때, NaN이 발생하면 이것이 어디서부터 온 것인지 추적하기가 어려울 때가 많다. 그래서 NaN을 마주치면 빠르게 원인을 살펴보고 적절한 예외 처리를 하는 것이 좋다.

이처럼 특별값들은 컴퓨터 내부에서 연산을 계속 진행할지, 혹은 프로그램이 즉시 멈춰야 할지에 대한 로직을 분기할 때 중요한 단서를 제공한다. 예를 들어 루프 도중에 NaN이 발생하면, 반복을 진행할 의미가 없으므로 중단해야 할 수도 있다. 어떤 환경에서는 NaN이나 무한대로 값을 던지기보다는 예외(exception)를 발생시키도록 설정할 수도 있다.

#### 서브노멀(Subnormal) 수

IEEE 754 표준에서는 지수부가 최소값에 도달했음에도 불구하고, 유효숫자부의 가장 앞 비트(리딩 비트)를 1로 고정하지 않는 방식을 채택하여, 극도로 작은 값을 추가로 표현할 수 있도록 한다. 이것이 서브노멀(denormalized or subnormal) 수이다. 보통 부동소수점은 정규화(normalized) 형태로 표준화되어, 유효숫자부가 $1.xxx\dots\_{2}$ 꼴을 유지한다. 그러나 지수부가 너무 작아 더 이상 감소할 수 없는 상황에서도, 유효숫자부의 리딩 비트를 1이 아닌 0으로 두고 남은 비트만으로 값을 표현하면, 약간이나마 더 작은 값을 나타낼 수 있다.

예컨대 단정밀도에서 지수부가 모두 0이면, 보통 $e - \text{bias} = -126$으로 보고 유효숫자부는 정규화된 $1.\dots$가 아니라 $0.\dots$ 형태로 간주한다. 이 값들은 $\pm 2^{-149}$처럼 매우 작은 크기까지 표현하는 데 기여한다. 하지만 이 영역에서 정밀도가 더 떨어지고, 연산 과정 중에 의도치 않은 계산 편차가 커지기도 한다. 서브노멀 수들은 정규화된 범위를 벗어난 작은 수를 어느 정도 처리할 수 있게 해주지만, 하드웨어 구현 상 느리게 동작하거나 추가 회로가 필요한 경우가 있다.

서브노멀 수를 다루는 환경에서는, 수치해석 알고리즘에서 매우 작은 수가 실제로 서브노멀 범위에 들어가는지, 그리고 그로 인해 반올림오차나 성능 저하가 발생할지 고려해야 한다. 어떤 환경은 서브노멀을 플러시 투 제로(Flush to Zero) 옵션으로 처리하여, 지수부가 최소값 이하인 경우 전부 0으로 보고 빠르게 연산하기도 한다. 이 방법은 일부 정밀도를 희생하는 대신 계산 효율을 높인다.

#### 머신 엡실론(Machine Epsilon)

부동소수점 시스템에서, $1$과 그 다음 표현 가능한 수 사이의 차이를 가리키는 기준 간격을 머신 엡실론(machine epsilon)이라고 한다. 특히 IEEE 754 표준에서 단정밀도의 머신 엡실론은 대략 $2^{-23}$이고, 배정밀도에서는 $2^{-52}$ 정도다. 흔히 10진수로 환산할 때 단정밀도는 약 $1.19209 \times 10^{-7}$, 배정밀도는 $2.220446 \times 10^{-16}$로 알려져 있다.

머신 엡실론은 수치해석에서 상대 오차의 척도가 된다. 예컨대 $1 + \epsilon$에서 $\epsilon$이 머신 엡실론보다 작으면, 부동소수점 연산에서 $1 + \epsilon = 1$로 반올림되어 구분되지 않는다는 의미다. 그래서 어떤 알고리즘의 안정도를 분석할 때, 상대 오차 항에 머신 엡실론이 등장하는 경우가 잦다.

머신 엡실론은 $1.0$ 근처에서의 정밀도를 측정하는 기준이지만, 실제로는 수의 크기마다 절대 오차 척도가 달라질 수 있으니 주의해야 한다. 예컨대 매우 큰 수와 작은 수를 더할 때, 큰 수 쪽에서 보면 상대 오차가 무시될 수도 있지만, 작은 수 쪽에서는 완전히 사라지는 꼴이 된다.

수학적 정의에서 머신 엡실론은 다음과 유사하게 표현된다.

$$
\epsilon\_{\text{mach}} = \min { \epsilon : 1 + \epsilon \neq 1 }
$$

이 정의에서 최소값을 구할 때, 실제 부동소수점 환경 내에서 해를 직접 이분 탐색으로 찾는 방식도 흔히 소개된다.

#### 유닛 라운드오프(ULP: Unit in the Last Place)

ULP는 특정 구간에서 부동소수점으로 표현 가능한 값들의 간격을 의미하며, 한 번의 반올림에서 변동될 수 있는 최소 단위를 말한다. 어떤 수 $x$가 있을 때, $x$와 가까운 다음 표현 가능한 수 $x\_{\text{next}}$ 사이의 차이를 ULP로 볼 수도 있고, $x\_{\text{prev}}$와의 차이를 고려할 수도 있다. 보통은 반올림 과정에서 수가 올라갈지 내려갈지를 결정짓는 간격이므로, 부동소수점 환경에서 오차의 척도가 된다.

ULP는 $1.0$ 근방에서는 머신 엡실론과 유사한 크기를 가지지만, 더 큰 수나 더 작은 수에서는 표현 간격 자체가 달라진다. 이는 부동소수점 표현에서 지수가 달라질 때마다 유효숫자부가 유지되는 자릿수만 동일하기 때문이다. 예컨대 $2^n$ 근처의 ULP는 $2^{n - p}$ 꼴이 될 수 있는데, 여기서 $p$는 유효숫자부가 표현하는 비트 수다. 즉, 절대 오차 범위가 지수에 따라 달라지므로, 상대 오차의 크기는 변동이 크다.

#### 반올림 모드(Rounding Mode)

IEEE 754 표준은 기본적으로 절반값을 가장 가까운 짝수로 반올림하는 규칙(Round to Nearest, Ties to Even)을 사용하지만, 아래와 같은 여러 모드를 지원한다.

* Round toward zero: 양수와 음수 모두 0에 가깝게 잘라낸다.
* Round up(+∞ 쪽으로): 모든 양수 연산은 올림, 음수 연산은 버림과 동일.
* Round down(-∞ 쪽으로): 모든 양수 연산은 버림, 음수 연산은 올림과 동일.
* Round to nearest, ties to even: 가장 가까운 수로, 절반이면 짝수로.

이 중에서 디폴트인 ties to even 모드는 통계적으로 오차가 한쪽으로 치우치지 않도록 하는 장점이 있다. 다른 모드를 강제로 쓰면 예측 가능한 특정 방향으로 오차를 유지하거나, 재현성을 확보하기도 한다. 예컨대 금융 계산 등에서 소수점 반올림 규칙을 명시적으로 고정해야 하는 경우에는 필요하다.

하지만 수치해석 측면에서는 ties to even이 가장 보편적이다. 덧셈이나 뺄셈 시, 올림과 버림이 균형 잡혀야 오차가 한 방향으로 크게 누적되지 않기 때문이다. 다른 모드를 선택할 때는, 알고리즘의 특성과 응용 목표를 충분히 검토해야 한다.

#### 확장 정밀도(Extended Precision)

일부 CPU나 수치 연산 환경에서는 내부 레지스터에서 더 많은 비트로 연산을 수행하고, 메모리에 저장할 때만 단정밀도나 배정밀도로 줄이는 ‘확장 정밀도’를 활용한다. 예컨대 x86 아키텍처의 x87 FPU에서는 80비트 내부 레지스터를 사용해 더 높은 정밀도를 임시로 유지하고, 필요한 순간에 64비트나 32비트로 저장한다. 이로 인해 계산 과정에서 반올림오차가 조금 더 늦게 누적되며, 결과적으로 오차가 줄어드는 이점이 있다.

그러나 확장 정밀도가 반드시 이롭기만 한 것은 아니다. 우선, 컴파일러나 언어 환경에 따라 확장 정밀도를 제대로 활용하지 못하는 경우도 있다. 더구나 어떤 부분에서는 80비트 레지스터를 쓰다가, 중간에 일시적으로 64비트로 떨어뜨린 뒤 다시 레지스터로 복사하면 그 과정에서 오차가 발생하거나, 예기치 않은 결과가 나올 수 있다. 예를 들어 중간 계산까진 80비트로 이루어졌는데, 어떤 이유로 스택에 저장했다가 다시 불러오면 그 순간 정밀도가 64비트로 끊겨서 이후 계산과의 일관성이 바뀌는 식이다.

현대의 CPU 벡터화 명령(SSE, AVX 등)은 보통 배정밀도(64비트) 또는 단정밀도(32비트)를 기본으로 지원하므로, 과거 x87 FPU 방식의 확장 정밀도를 그대로 쓰지 않는 추세다. 대신 소프트웨어적으로 확장 정밀도 연산을 흉내 낼 수 있도록 특수한 라이브러리나 커스텀 타입을 제공하는 경우가 있다. 이런 방식은 속도에서 손해가 크지만, 반드시 필요한 높은 정밀도가 요구될 때만 부분적으로 사용하기도 한다.

#### 고정밀도(Quadruple Precision)와 그 밖의 변형

배정밀도보다 더 높은, 예컨대 128비트 부동소수점(quadruple precision)을 쓰면 정밀도와 표현 범위가 크게 확장된다. IEEE 754-2008에서는 128비트 부동소수점 형식을 정의하며, 이를 통해 약 34자리 이상의 10진 유효숫자를 다룰 수 있다. 이 형식을 지원하는 하드웨어는 드물지만, 소프트웨어적으로 에뮬레이션해 사용하는 경우가 있다. 예컨대 GCC의 \_\_float128이나 일부 컴파일러 확장은 이런 기능을 제공한다.

128비트 고정밀도는 일반적 과학 컴퓨팅에서 크게 쓰이지 않지만, 매우 예민한 수치적 문제나 고차적 통계 계산, 다항 방정식의 극정밀 해 해석 등에서 필요할 수 있다. 물론 이는 연산 속도가 크게 떨어지고, 메모리 사용량도 증가한다. 그래서 이를 현장에서 적용할 때, 어떤 단계에서만 국소적으로 128비트를 쓸지, 혹은 전체 알고리즘을 통째로 128비트로 돌릴지 치밀하게 검토한다.

최근 GPU나 머신러닝 분야에서는 반정밀도(half precision, 16비트)나 BFLOAT16(1비트 부호, 8비트 지수, 7비트 유효숫자) 형태가 주로 부각된다. 이것들은 정밀도가 낮지만 연산 속도가 빠르고 메모리 대역폭 절약이 가능하기 때문에 딥러닝 훈련 등에 유리하다. 결과적으로 배정밀도와 단정밀도, 반정밀도, 혼합정밀도가 뒤섞인 형태의 하드웨어 스펙과 라이브러리가 대두되었고, 이를 수치해석 관점에서 어떻게 활용할 것인가도 중요한 연구 주제가 되었다.

#### 십진 부동소수점(Decimal Floating-Point)

금융이나 회계 분야 등에서는 2진 부동소수점이 아니라 10진 부동소수점을 사용하는 것이 더 적합할 때가 많다. 10진 부동소수점(decimal floating-point)은 10진 정수부와 소수부를 직접 다루므로, 예를 들어 $0.1$이나 $0.2$를 정확하게 표현할 수 있다. IEEE 754 표준도 10진 부동소수점을 정의하지만, 실제 하드웨어나 소프트웨어 지원은 2진 부동소수점만큼 광범위하지 않다.

언어 차원에서 decimal 타입을 제공하는 경우도 있고, 별도의 라이브러리로 구현한 소프트웨어 십진 부동소수점을 사용하기도 한다. 이 방법은 은행 계좌 잔액 계산처럼 10진 기반에서 소수점 반올림을 엄격히 다뤄야 하는 곳에서 특히 중요하다. 대신 2진 부동소수점보다 연산 속도가 느리고, 구형 프로세서에서는 하드웨어 가속이 없으므로 오버헤드가 크다. 또한 10진 부동소수점 역시 최대 자리수가 제한되어 있으므로, 근본적으로 임의정밀도 방식을 대체하지는 못한다.

#### 하드웨어 가속과 오차의 관계

GPU나 벡터화 확장(SIMD) 같은 하드웨어 가속을 적용할 때, 한 번에 여러 데이터 요소를 병렬 연산하므로 오차 양상이 기존 스칼라 방식과 달라질 수 있다. 병렬 연산 순서가 바뀌면 덧셈처럼 교환법칙이 이론적으로 성립하는 연산도, 부동소수점 환경에서는 결과가 달라질 수 있다. 수학적으로 $a + b = b + a$가 성립하지만, 실제 부동소수점 반올림이 들어가면 연산 순서가 미세하게 다른 결과를 낼 가능성이 있다.

이런 현상 때문에 병렬 알고리즘 디버깅이나 재현성(reproducibility) 문제가 자주 부각된다. 같은 코드를 같은 입력으로 돌려도, 쓰레드 스케줄링에 따라 연산 순서가 달라지면 결과가 조금씩 달라질 수 있다. 보통 그 차이가 머신 엡실론 수준이라면 큰 문제가 아니지만, 민감한 계산에서는 실제로 결과가 크게 달라질 수도 있다. 따라서 병렬 계산에서는 순서를 강제하거나, 결과를 적당히 안정화하는 기법(예: Kahan summation, pairwise summation)을 통해 오차 누적을 줄이려 노력한다.

하드웨어 벡터화가 활발해진 최근에는, 높은 정밀도보다는 빠른 연산에 초점을 맞추는 흐름이 강하다. 이 때문에 작은 정밀도 타입(예: FP16, TF32 등)을 대규모로 굴리면서, 필요한 부분만 혼합정밀도로 보강하는 형태가 많다. 수치해석적으로는 이러한 시도를 통해 오차가 큰 방향으로 치우치지 않도록 제어하는 아이디어가 필요하다.

#### 언어별 자료형 프로모션과 자동 변환

컴파일러나 프로그래밍 언어는 서로 다른 자료형 간 연산을 수행할 때, 특정 규칙에 따라 상위 정밀도 타입으로 승격(promote)하거나 반대로 하위 정밀도 타입으로 강제 변환하기도 한다. 예컨대 C/C++에서 `float`와 `double`을 함께 연산하면 보통 `double`로 승격되는 식이다. 그런데 이런 규칙이 상황에 따라 예외가 있을 수 있으며, 특정 플랫폼의 ABI(Application Binary Interface)에 따라 달라지는 사례도 있다.

자동 변환 과정에서 손실이 발생할 수 있다. 예를 들어, `double`로 계산한 결과를 다시 `float`에 담으면 `float`의 정밀도에 맞춰 반올림된다. 이로 인해 중간 계산까진 배정밀도로 이뤄져 오차가 상대적으로 작았어도, 최종 결과를 저장하는 시점에서 오차가 커질 수 있다. 만약 이후 연산이 다시 `double`로 승격된다면, 이미 손실된 정밀도를 복원할 수 없으므로 계산 과정이 꼬일 여지가 생긴다.

언어마다 `long double`이나 `decimal` 등 여러 특수 타입을 제공하기도 하지만, 실제 표현 형식이 하드웨어 의존적이어서 예상치 못한 오차 양상이 드러날 수 있다. 예컨대 GCC에서 `long double`은 보통 80비트 확장 정밀도를 쓰지만, 일부 환경에서는 128비트 소프트웨어 에뮬레이션이 될 수도 있다. 따라서 이식성을 확보하려면 자료형 프로모션 규칙과 구현 환경을 사전에 점검해야 한다.

#### 정수 오버플로우 방지 기법

정수 연산에서 오버플로우는 알고리즘 결과 전체를 왜곡할 수 있다. 예를 들어, 어떤 누적 합이 $2^{31} - 1$을 넘어서면 부호가 음수가 되거나 예기치 못한 값이 튀어나온다. 이를 방지하기 위해 다양한 기법이 존재한다. 대표적으로는, 누적 합을 취할 때 더 큰 폭의 정수형(예: 64비트)으로 중간값을 저장하거나, 연산 전후에 범위 검사를 수행하는 방법이 있다.

언어 수준에서 오버플로우를 탐지할 수 있는 확장 기능이나 라이브러리를 쓰기도 한다. 예컨대 C++20에 추가된 `std::midpoint`는 덧셈 과정에서 오버플로우를 방지해 중간값을 계산해 준다. 때로는 정수 범위를 넘어설 가능성이 전혀 없도록 문제를 재구성하는 방식도 쓴다. 부동소수점으로 처리해야 할 연산을 정수로 억지로 우회하지 않는 편이 바람직할 때도 많다.

#### 부동소수점 비교 시 주의사항

부동소수점 수를 직접 비교할 때는, 반올림오차가 항상 잠재되어 있음을 염두에 두어야 한다. 예를 들어, $x$와 $y$를 계산한 뒤에 $x == y$로 비교하면, 매우 작은 오차로 인해 실제로는 같음에도 불구하고 다르다고 판단할 수 있다. 따라서 $|x - y| < \delta$ 형태의 방법으로 근사 비교를 수행하는 것이 일반적이다. 여기서 $\delta$는 수치적 상황에 맞춰 적절하게 설정해야 한다.

상대 오차 비교를 하는 경우, 예컨대

$$
\frac{|x - y|}{\max(|x|, |y|, 1)} < \epsilon
$$

같은 방식을 쓰기도 한다. 이렇게 하면 값이 매우 작거나 매우 큰 경우에도 적절히 대응할 수 있다. $\epsilon$은 보통 머신 엡실론이나 문제 요구 정밀도 수준으로 잡는다. 다만, 알고리즘적 맥락에 따라 정확히 얼마나 엄격한 비교가 필요한지가 달라지므로, 과도하게 작은 $\epsilon$을 설정하면 불필요한 연산 반복이나 데이터 거부가 일어날 수 있다.

#### 오차 추적과 로그(logging)

수치해석 알고리즘을 작성할 때, 중간 단계에서 값이 무한대나 NaN으로 튀는 등 오류가 발생하면 이를 빠르게 감지해야 한다. 간단한 방법으로는 중요한 연산 후에 검사 코드를 넣어, NaN이나 무한대가 되었는지 확인하고 문제가 생기면 로그 파일에 남기거나 곧바로 예외 처리를 하는 식이다.

오류 상황이 발생했을 때, 어느 단계에서 어떤 값이 어떻게 왜곡되었는지 추적하기는 쉽지 않다. 특히 병렬 연산이나 비동기 처리가 얽히면 재현성 문제가 생겨, 매번 같은 순서로 똑같은 결과가 나오지 않을 수 있다. 이런 경우, 디버그 모드에서 덧셈 순서를 일부러 고정시키거나, 임의의 체크포인트에서 중간 상태를 덤프하여 나중에 재분석하는 방법이 유용하다.

데이터 로깅 시, 지나치게 정밀한 값을 모두 기록하면 메모리나 스토리지 사용량이 커지고 속도가 느려지므로 균형점을 찾는 것이 중요하다. 필요하면 극히 일부 연산 단계에서만 고정밀 추적을 수행할 수 있도록 구현하기도 한다.

#### 병렬 환경에서의 재현성(Reproducibility) 문제

병렬 계산에서는 덧셈이나 뺄셈 같은 연산의 순서가 실행마다 달라질 수 있으며, 이로 인해 결과가 약간씩 달라질 수 있다. 수학적으로는 가환 연산이지만, 부동소수점 환경에서 반올림 방식이 개입하면 순서에 따라 다른 반올림 누적 결과가 얻어진다. 예를 들어 대규모 벡터의 합을 여러 스레드가 나눠 처리한 뒤, 최종적으로 결합한다면 결합 순서가 바뀔 때마다 결과가 달라질 수 있다.

어떤 응용에서는 이런 차이가 무의미할 수 있지만, 금융이나 원자 단위 물리 시뮬레이션처럼 작은 변화도 중요한 분야에서는 문제가 된다. 재현성을 보장하기 위해 누적 합이나 행렬 곱셈 순서를 고정하거나, 한 스레드에서만 최종 합산을 맡게 하는 등 특정 통제 전략이 필요하다. 하지만 이런 전략은 병렬 성능을 낮출 수 있으므로, 정확도와 속도 사이의 절충안이 요구된다.

#### 실무적 자료형 선택 전략

알고리즘 설계 시, 우선적으로 고민해야 할 것은 연산 범위와 필요한 정밀도의 수준이다. 값이 매우 크게 혹은 매우 작게 변동할 가능성이 있다면 부동소수점 사용이 자연스럽다. 다만, 정밀도가 너무 부족하면 중간 단계에서 문제 해결이 불안정해질 수 있으므로 배정밀도 이상의 형식을 고려하거나, 혼합정밀도 기법을 적극 활용해야 한다.

반면, 정수 단위 계산으로 충분하고 속도가 중요한 경우라면 과감히 정수 자료형을 쓰고, 범위 점검을 철저히 해서 오버플로우를 예방한다. 임베디드 환경 등에서 고정소수점을 쓰는 경우는 메모리 비용이 크게 절감되며, 하드웨어 가속과 맞물려 높은 처리량이 보장되는 장점이 있다. 그러나 이때는 표현 범위와 소수부 비트 수를 초과하는 입력이 들어오지 않도록 철저히 관리해야 한다.

또한, 극단적으로 높은 정밀도가 필요한 문제는 임의정밀도나 128비트 고정밀도 등을 고려할 수 있으나, 성능이 급격히 떨어진다. 실제로는 알고리즘적 개선을 통해 극한 상황을 피하거나, 수학적 전처리를 통해 문제를 재구성하여 표준 배정밀도로도 충분히 해결할 수 있는 경우가 많다. 결국 적용 분야, 성능 요구 사항, 데이터 범위를 종합적으로 살피는 전략이 필요하다.

#### 테스트와 검증(Validation) 방법

자료형에 따른 오차 발생을 실제로 측정하고 이해하기 위해서는, 다양한 테스트와 검증 기법이 필요하다. 예컨대 특정 알고리즘의 출력을 이론적 참값(analytic solution)이나 고정밀도 연산 결과(예: 임의정밀도 라이브러리로 구한 값)와 비교하여 오차의 크기를 살펴볼 수 있다. 비교 시에는 단순히 $|x - y|$ 같은 절대 오차가 아니라, 상대 오차나 참값과의 비율을 고려해 보는 것이 좋다.

대부분의 수치해석 문제에서 완전히 정확한 참값을 구하기 힘들 때가 많다. 이 경우, 알고리즘 자체의 수렴 속도나 이미 알려진 문헌의 결과값(benchmarks) 등을 통해 오차 발생 범위를 추정하기도 한다. 예를 들어, 특정 PDE(편미분방정식)를 해석적으로 풀 수 없는 경우, 그 문제를 충분히 작은 크기로 분할한 뒤 임의정밀도 계산으로 근사한 해를 ‘가장 정밀한 기준값’으로 삼는다. 이후 실질적으로 사용하는 자료형으로 계산했을 때 얼마나 차이가 나는지 살펴보면서 적절한 정밀도와 알고리즘을 결정한다.

프로파일링 툴이나 디버거를 활용해 중간값들을 추적하고, NaN이나 ±∞가 발생했을 때 해당 지점의 데이터를 덤프해 보는 것도 중요하다. 어떤 입력 집합에서만 특이하게 오차가 커지거나 오류가 나타나는 경우가 있는데, 이를 재현 가능한 최소 예시(Minimal Working Example)로 축소하여 면밀히 살피면 소프트웨어 버그 혹은 자료형 사용상의 문제점을 파악하기가 한결 쉬워진다.

#### 수치적 안정성(Numerical Stability)과 자료형

알고리즘이 어떤 자료형을 쓰든, 기본적인 수치적 안정성 개념을 먼저 점검해야 한다. 예를 들어, 아주 비슷한 수끼리 뺄셈하는 연산이 잦다면(소위 catastrophic cancellation), 부동소수점 반올림오차가 증폭되어서 결과 신뢰도가 급격히 떨어질 수 있다. 설령 배정밀도를 쓰더라도, 안정성이 낮은 알고리즘이라면 오차가 쉽게 부풀려진다.

따라서 자료형 선택에 앞서, 가능한 안정한 알고리즘(Stable Algorithm)을 채택하는 것이 최선이다. 예컨대 다항식을 직접 계산하기보다, 동등하지만 수치적으로 안정한 재귀식이나 분할정복 형태로 계산할 수 있는지 살핀다. 연립방정식을 풀 때도, Gauss 소거법 자체가 불안정할 수 있으니 부분 피벗팅을 도입하거나, QR 분해나 SVD 같은 방법을 통해 안정성을 확보하기도 한다.

안정한 알고리즘 위에서 단정밀도를 사용하면 오차가 어느 정도 수용 가능한 수준에서 머무를 가능성이 커진다. 반면, 불안정한 알고리즘이라면 배정밀도나 그 이상을 써도 여전히 심각한 결과가 나올 수 있다. 결국 자료형은 보조적인 수단이며, 근본적으로 알고리즘 선택이 오차 수준을 지배한다는 점을 유념해야 한다.

#### Kahan Summation 기법

부동소수점 덧셈에서 가장 빈번하게 일어나는 문제가, 큰 수와 작은 수의 합에서 작은 수가 제대로 반영되지 못하고 반올림되어 사라지는 현상이다. 이를 완화하기 위해 Kahan Summation이라는 알고리즘이 자주 인용된다. 간단한 예제 코드를 Python으로 보여주면 다음과 같다.

```python
def kahan_sum(values):
    s = 0.0
    c = 0.0
    for x in values:
        y = x - c
        t = s + y
        c = (t - s) - y
        s = t
    return s

vals = [0.000001]*1000000
print("Normal sum =", sum(vals))
print("Kahan sum  =", kahan_sum(vals))
```

여기서 `s`는 누적합, `c`는 보정 항(correction term)이다. 매번 새로운 항 `x`를 더하기 전에, 이전 단계에서 사라졌을 수 있는 오차를 `c`로 추적하고, 이를 이용해 실제 연산에 반영한다. 일반적인 단순 합산보다 정확도가 눈에 띄게 개선된다. 이런 방법은 ‘도적 잡기(catching lost bits)’ 기법으로도 불린다.

물론 Kahan Summation조차 완벽하지 않으며, 값이 음수와 양수가 뒤섞여 있을 때나 매우 큰 스케일 차이가 날 때는 효과가 제한적이다. 그래도 단순 누적합보다는 훨씬 안정적이므로, 여러 과학 컴퓨팅 라이브러리에서 옵션으로 제공한다.

#### 연산 재배열(Rearranging Operations)을 통한 오차 완화

수학적으로 동등한 표현이더라도, 부동소수점 연산에서는 그 순서나 재배열 여부에 따라 결과 오차가 달라질 수 있다. 예컨대 합 $a + b + c$를 $(a + b) + c$와 $a + (b + c)$ 중 어느 순서로 평가하느냐에 따라 반올림 방향이 약간씩 달라진다.

수치해석 문제를 해결할 때는, 오차가 크게 발생할 가능성이 높은 연산(예: 서로 근접한 두 큰 수의 뺄셈)을 뒤로 미루거나 다른 방식으로 재배열하여 문제를 완화할 수 있다. 이런 기법은 단순 합산뿐 아니라, 미분 방정식 솔버나 적분 계산 등 여러 분야에서 응용된다. 예컨대 Taylor 전개를 이용해 함숫값을 구할 때, 지수나 로그가 큰 경우를 다루는 순서를 최적화하여 연산 순서를 바꾸는 식이다.

다만, 컴파일러 최적화가 자동으로 연산 순서를 바꿀 수도 있고, 이는 결과를 재현하기 어렵게 만든다. 그래서 과학 계산 라이브러리는 대개 ‘fast-math’ 옵션처럼 공격적인 최적화를 제한하거나, 특정 블록에서는 정밀 계산을 우선하도록 명시하기도 한다. 반올림 모드나 FPU 환경을 고정해 두고, 최적화 세부 사항을 통제함으로써 재현성과 예측 가능성을 확보한다.

#### 매개변수 스케일링(Scaling)과 정규화

수학 문제를 풀기 전에 입력값을 적절히 스케일링(scaling)하여, 데이터가 ‘적당히 큰’ 범위로 오도록 맞추면 부동소수점 오차를 줄일 수 있다. 예를 들어, $10^6$ 정도의 수와 $10^{-6}$ 정도의 수가 뒤섞여 있다면, 연산 중에 작은 항이 크게 무시될 가능성이 높다. 이를 미리 단위 변환이나 정규화(normalization)로 보정하면, 비슷한 크기끼리 연산하도록 재배치할 수 있다.

선형대수 문제에서 행렬 A의 조건수(condition number)가 크다면, 스케일링을 통해 이를 줄이는 기법이 효과적일 수 있다. 예컨대 행렬의 각 행이나 열을 특정 기준에 맞춰 정규화하면, 연립방정식 풀이에 필요한 연산들이 보다 균형 잡힌 형태로 진행된다. 이런 사전 스케일링 기법은 수치적 안정성을 크게 높이고, 필요한 자료형 정밀도도 낮출 수 있게 해 준다.

#### 정리

자료형 선택과 오차 분석은 불가분의 관계이며, 현대 컴퓨터 환경에서는 32비트부터 128비트, 심지어 임의정밀도까지 다양한 형태가 혼재해 있다. 이 과정에서 부동소수점 반올림, 정수 오버플로우, 서브노멀 수, 혼합정밀도 등 복잡한 이슈들이 얽히므로, 각 단계에서 어떤 형식을 쓰고 어떤 오차가 발생할 수 있는지를 미리 인지해야 한다.
