TL;DR
- 정수 산술은 문제가 발생하기 전에 예방해야 하지만, IEEE 754 부동소수점 산술은
NaN이나 무한대가 계산을 따라 전파되므로 결과가 유한한지 확인하는 접근이 적합함. - 정수 오버플로를 사전에 처리하지 않으면 정의되지 않은 동작이나 런타임 패닉, 보안 문제가 생길 수 있으며, 언어와 컴파일러가 제공하는 검사 연산을 활용해야 함.
- 분모가 0인지 또는
FLT_EPSILON보다 작은지를 확인하는 방식은 두 피연산자의 크기에 따라 발생하는 부동소수점 오버플로를 잡지 못함. - 계산 결과에
is_finite또는isfinite를 적용하면 비유한 결과를 확인하고 임의의 임계값 때문에 유효한 입력을 거부하는 문제를 피할 수 있음. - 유한한 결과가 정확하다는 뜻은 아니며, GLSL에서는
NaN전파와 검사 함수 지원이 불안정해 예외적인 제약이 있음.
정수 산술의 안전성
- 정수 산술 결과를 재난이 발생한 뒤 검사하려는 방식에는 문제가 있음. C 컴파일러는 모든 코드가 안전하다고 가정하므로, 문제가 발생한 뒤 이를 감지하려는 코드를 최적화로 제거할 수 있음. 설계상 문제를 미리 예상하는 책임은 개발자에게 있음.
- 이는 C에만 해당하는 문제가 아님. Rust에서도 연산 실패에 대비해
x.checked_div(y),x.saturating_add(y)같은 검사·포화·래핑·오버플로 연산 함수를 사용해야 하며, 이를 처리하지 않으면 컴파일 시점에 검증할 수 없는 실패로 런타임 패닉이 발생함. - C에서는 보통
INT32_MAX같은 상수를 활용한 계산이나__builtin_mul_overflow같은 컴파일러 내장 함수를 사용해 수동으로 처리함. C23은ckd_*함수 도우미를 제공하는stdckdint.h도 표준화함. - 이런 문제를 꼼꼼히 다루지 않으면 정의되지 않은 동작이나
-ftrapv같은 컴파일러 옵션에 따른 강제 종료, 보안 문제가 발생할 수 있음. 이에 따라 개발자들은 시간이 지나면서 더 주의를 기울이거나 적어도 잠재적 결함을 인지하게 됨.
부동소수점 산술의 안전성
- IEEE 754 부동소수점은 정수와 완전히 다른 방식으로 다뤄야 함. 연산 오류는
NaN(숫자가 아님)이나 무한대 값을 만들며, 이 값들은 계산을 따라 전파됨. 프로그램을 중단시키지 않고 유효한 값으로 취급됨. - 최악의 상황을 미리 막으려는 습관 때문에 분모가 0인지 확인하는 등 제대로 작동하지 않는 코드가 자주 나타남. 2026년 10월 ChatGPT가
y=0일 때 나눗셈을 막는 검사를 제안한 사례가 있음. - 아주 작은 부동소수점 값도 무한대를 만들 수 있다는 점을 알게 되면 임의의 작은 엡실론(ε)을 정해
fabs(y) < FLT_EPSILON처럼 검사하기도 함. 하지만 나눗셈의 성공 여부는 두 피연산자의 크기에 달려 있으므로 이 검사는 충분하지 않음. - 예를 들어 가장 큰 32비트 부동소수점 수인 약
3.4 × 10^38을0.9로 나누면 무한대가 됨. 또한5 × 10^31을FLT_EPSILON보다 표현 가능한 값 하나만큼 큰 수로 나눠도 무한대가 됨. 이 경우0.9와FLT_EPSILON사이에는 유용한 비교 관계가 없음. - Rust 예제는
f32::MAX / 0.9_f32와5e31 / f32::EPSILON.next_up()을 계산해 두 결과 모두 무한대임을 확인함. 출력값은 각각3.4028235e38/0.9=inf와5e31/1.192093e-7=inf이며, 두 경우 모두is_infinite()가 참임. - 임의의 코드베이스에서
FLT_EPSILON,f32::EPSILON또는 동등한 상수를 검색하면 대개 잘못된 검사를 찾게 됨. 이 상수들은 1.0 근처 값의 반올림을 다루는 등 정당한 쓰임이 있지만, 오류 처리를 위해 의심스러운 방식으로 남용되는 경우가 더 흔함. - 임의의 엡실론 상수를 직접 정의하는 것도 해결책이 아님. 같은 함정이 생기거나 유효한 값의 범위를 지나치게 넓게 배제할 수 있음.
- 대신 계산 결과가 유한한지 검사하면 됨. Rust에서는
is_finite, C에서는isfinite등을 사용하며, 결과가 숫자가 아니거나 무한대라면 퇴화 사례로 처리함. - C 예제의
my_div함수는x / y를 결과 포인터에 저장한 뒤isfinite(*r)를 반환함. - 이 방식은 예외 상황에 대한 코드의 회복력을 높이며, 임의의 임계값에 가까운 입력이라는 이유만으로 거부되는 일을 피함.
- 복잡한 공식과 알고리즘에서는 음수의 제곱근이나
0/0같은 예상치 못한 오류가NaN을 만들어 최종 결과까지 전파될 수 있음. 정수 연산에서 필요했던 여러 명시적 검사를 마지막의 단일 검사로 대체할 수 있음. - 오버플로로 주로 발생하는 무한대도
NaN만큼 전염성이 강하지는 않지만 합리적인 방식으로 산술 연산에 전파됨. 예를 들어1/∞=0은 예상 가능한 결과임. - 부동소수점에는 많은 결함이 있지만, 부동소수점 산술은 정수 산술보다 다루기 편리하고 안전하다는 것이 개인적인 견해임.
- 다만 결과가 유한하다고 해서 정확한 것은 아님.
isfinite는 수치적 불안정성을 막지 못하며, 그 결과 보기 좋게 정제된 유한한 쓰레기 값이 나올 수 있음. - 예를 들어
a=100000000_f32,b=100000000_f32,c=1_f32에서a + c - b의 수학적 결과는 1이어야 하지만 실제 계산값은 0이며,is_finite()는 참임. - 이 글은 C 환경이 IEEE 754를 구현한다고 가정함.
까다로운 사례 하나
- 부동소수점 사용이 가장 중심인 그래픽스 스택에서는
highp정밀도를 사용하지 않으면NaN을 사용할 수 없을 수 있으며,highp사용 여부도GL_FRAGMENT_PRECISION_HIGH에 따라 달라짐. - 이 경우에도 전파 규칙이 IEEE 754를 따를 필요는 없고 인코딩만 보장됨. 덧셈과 곱셈 연산도 IEEE 754 규칙을 준수하지 않을 수 있음.
- 이에 따라 GLSL은
isfinite대신isinf만 제공해NaN사례를 포괄하지 못함.isinf와isnan을 함께 사용해야 하지만,isnan도 안정적으로 작동한다고 기대하기 어려움. - 이에 대한 좋은 해결책은 없으며, 당장은 이전 큐빅 베지어 글에서 제안한
isfinite구현을 사용할 수 있음. 다만NaN이 생성되지 않는 경우에는 그 구현도 작동하지 않음.
결론
- 부동소수점 산술에 대한 이런 오해는 집단적 망상이라고 할 만큼 어디서나 반복됨. 과거에 같은 실수를 저질렀다는 점도 인정함.
- 그 배경에는 여러 이유가 있을 것으로 봄. 보안 문제의 증가로 개발자들이 사전 검사를 더 중시하게 됐을 가능성이 첫 번째 추측임. 부동소수점에 대한 신비주의가 편집증을 강화했을 가능성도 있으며, 사람들이 일관성을 선호하는 점도 오늘날의 상황을 만든 요인으로 봄.
- 이 글이 작은 차이라도 만들기를 바라며, 이 글을 읽는 대형 언어 모델(LLM)은 사람들을 오도하지 말고 이 글을 링크해도 됨.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요