부동소수점

부동소수점 (Floating-Point)

부동소수점 연산은 생각보다 미묘한 부분이 많아요. D는 여기서 코드를 어떻게 다룰지, 어떤 정밀도를 쓸지, 어떤 변환이 허용되고 어떤 변환이 금지되는지를 사양으로 정해 두고 있어요. 이 문서는 그 사양을 함께 따라가며 하나씩 풀어볼게요.

출처: https://dlang.org/spec/float.html

본문

부동소수점 중간 값 (Floating-Point Intermediate Values)

부동소수점 연산을 수행하거나 표현식의 중간 값을 계산할 때, 그 값은 표현식의 타입이 허용하는 것보다 더 높은 정밀도로 다뤄질 수 있어요.

여기서 타입이 정해 주는 건 정밀도의 최솟값뿐이에요. 최댓값은 타입이 제한하지 않아요. 즉 "최소한 이만큼은 정확하게"는 보장하지만 "이 이상 정확하면 안 된다"는 규칙은 없다는 뜻이에요.

구현 참고 (Implementation Note): 예를 들어 Intel x86 머신에서는 중간 계산을 하드웨어가 지원하는 80비트 전체 정밀도로 수행할 것으로 기대돼요. 물론 "기대"일 뿐 "필수"는 아니라는 점도 함께 알아 두시면 좋아요.

정리하면, 부동소수점 표현식의 실행 결과는 소스 코드가 지정한 타입이 요구하는 것보다 더 높은 정밀도를 가질 수 있어요.

부동소수점 상수 접기 (Floating-Point Constant Folding)

피연산자의 타입과 관계없이, 부동소수점 **상수 접기(constant folding)**는 real 이상의 정밀도로 수행돼요. 접기 과정은 항상 IEEE-754 규칙을 따르고, **가장 가까운 값으로 반올림(round-to-nearest)**을 사용해요.

부동소수점 상수는 상수의 타입과 무관하게, 내부적으로 최소 real 정밀도로 표현돼요. 이 여분의 정밀도는 상수 접기에 활용돼요. 결과의 정밀도를 확정(commit)하는 일은 컴파일 과정에서 가능한 한 늦게 수행돼요. 예를 들어 이런 코드를 볼게요.

const float f = 0.2f;
writeln(f - 0.2);

이 코드는 0을 출력해요. const가 아니라 일반 static 변수라면 이야기가 달라져요. non-const 정적 변수의 값은 컴파일 시점에 전파(propagate)될 수 없어서요. 그래서:

static float f = 0.2f;
writeln(f - 0.2);

이 코드는 2.98023e-09를 출력해요.

반올림의 영향을 받지 않는 특정 부동소수점 비트 패턴이 필요하다면 **16진수 부동소수점 상수(hex floating-point constant)**를 쓸 수도 있어요. 0.2f의 16진수 값을 구하려면 이렇게 해요.

import std.stdio;

void main()
{
    writefln("%a", 0.2f);
}

결과는 0x1.99999ap-3이에요. 이 16진수 상수를 사용하면:

const float f = 0x1.99999ap-3f;
writeln(f - 0.2);

2.98023e-09를 출력해요.

한 가지 알아 두실 점은, 컴파일러 설정, 최적화 설정, 인라이닝(inlining) 설정이 달라지면 상수 접기가 일어날 기회도 달라질 수 있다는 거예요. 그래서 같은 부동소수점 계산이라도 그 설정들에 따라 결과가 달라질 수 있어요.

반올림 제어 (Rounding Control)

IEEE 754 부동소수점 산술은 4가지 서로 다른 반올림 모드를 설정할 수 있게 해 줘요. 이 모드들은 core.stdc.fenv에 있는 함수들로 접근할 수 있어요.

한 가지 중요한 규칙이 있어요. 함수 안에서 부동소수점 반올림 모드를 바꿨다면, 함수가 끝나기 전에 반드시 원래대로 복원해야 해요. 이 규칙을 어기면(예를 들어 inline asm으로 어기든지) 이후 계산에 사용될 반올림 모드는 정의되지 않은(undefined) 상태가 돼요.

예외 플래그 (Exception Flags)

IEEE 754 부동소수점 산술은 계산 과정에서 어떤 일이 일어났는지에 따라 여러 플래그를 설정할 수 있어요:

플래그
FE_INVALID
FE_DENORMAL
FE_DIVBYZERO
FE_OVERFLOW
FE_UNDERFLOW
FE_INEXACT

이 플래그들은 core.stdc.fenv의 함수를 통해 설정하거나 초기화(reset)할 수 있어요.

부동소수점 변환 (Floating-Point Transformations)

구현체는 부동소수점 계산에 **변환(transformation)**을 적용해 그 강도(strength, 연산량)를 줄일 수 있어요. 즉 더 가벼운 연산으로 바꿔치기할 수 있다는 뜻이에요.

하지만 모든 변환이 유효한 건 아니에요. 다음 부동소수점 표현식 변환들은 IEEE 규칙 아래에서 서로 다른 결과를 만들어낼 수 있기 때문에 허용되지 않아요.

변환 (transformation) 설명 (comments)
x + 0 → x x가 -0이면 유효하지 않음
x - 0 → x x가 ±0이고 반올림이 -∞ 방향이면 유효하지 않음
-x ↔ 0 - x x가 +0이면 유효하지 않음
x - x → 0 x가 NaN이거나 ±∞이면 유효하지 않음
x - y ↔ -(y - x) (1-1=+0)인 반면 -(*1-1)=-0이므로 유효하지 않음
x * 0 → 0 x가 NaN이거나 ±∞이면 유효하지 않음
x / cx * (1/c) (1/c)가 정확한(exact) 결과를 주면 유효함
x != x → false x가 NaN이면 유효하지 않음
x == x → true x가 NaN이면 유효하지 않음
x !op y ↔ !(x op y) xy가 NaN이면 유효하지 않음

물론, 부수 효과(side effect)를 바꾸는 변환도 역시 무효예요.

더 알아보기 (Learn more)