---
title: "NaN 전파: 같은 결과 뒤에 다른 오류가 남는 이유"
author: "브레이브브라우저쓰자"
published_at: "2026-10-12T07:22:03.448Z"
updated_at: "2026-10-06T00:09:32.294Z"
tags: ["nan 전파"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/nan-%EC%A0%84%ED%8C%8C-%EA%B0%99%EC%9D%80-%EA%B2%B0%EA%B3%BC-%EB%92%A4%EC%97%90-%EB%8B%A4%EB%A5%B8-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%82%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0"
ai_assisted: true
---

# NaN 전파: 같은 결과 뒤에 다른 오류가 남는 이유

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/nan-%EC%A0%84%ED%8C%8C-%EA%B0%99%EC%9D%80-%EA%B2%B0%EA%B3%BC-%EB%92%A4%EC%97%90-%EB%8B%A4%EB%A5%B8-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%82%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0)

부동소수점 계산이 NaN을 내면 어디서 잘못됐는지 알 수 있을까. NaN은 Not a Number의 약자로, 해당 연산의 일반적인 실수 결과로 해석하지 않는 특별한 표현이다. 하지만 출력에 NaN이 있다는 사실만으로 입력의 NaN을 전달했는지, 새로운 invalid operation이 발생했는지 알 수는 없다.

NaN 전파를 이해하려면 **숫자 bit pattern과 exception flag를 함께 읽어야 한다.** 어떤 NaN을 선택하거나 정해진 형태로 바꾸는 정책도 구현 계약의 일부다.

## 같은 NaN 출력이 서로 다른 사건을 담는다

quiet NaN과 정상 유한 값을 더하면 보통 NaN 결과를 얻는다. 입력이 이미 연산 불가능한 표시를 담고 있으므로 그 표시가 계속 전달되는 경우다. quiet NaN이 있다는 이유만으로 새로운 invalid flag가 반드시 발생하는 것은 아니다.

반면 +infinity와 −infinity를 더하면 정상적인 합을 정의할 수 없어 invalid operation과 NaN 결과가 생긴다. signaling NaN 입력도 invalid flag를 유발하고 quiet 형태의 결과로 처리될 수 있다. 결과 표시가 같아도 오류가 발생한 사건은 다르다.

목표를 RISC-V binary32의 기본 NaN 정책으로 고정한 설명용 예는 다음과 같다. 이전 exception flag는 0이고, 각 연산을 별도로 관찰한다고 가정한다.

| 입력 사건 | 출력 | 새 invalid flag |
|---|---|---:|
| quiet NaN + 1 | canonical NaN | 0 |
| signaling NaN + 1 | canonical NaN | 1 |
| +infinity + −infinity | canonical NaN | 1 |

표는 특별한 입력 하나만 있는 덧셈의 의미다. FMA나 비교 등 다른 연산의 모든 NaN 조합을 같은 표로 처리할 수는 없다.

## payload를 보존할까, 하나의 형태로 만들까

binary32 NaN의 fraction 비트에는 quiet 여부와 payload로 사용할 수 있는 정보가 있다. [HardFloat의 NaN 결과 설명](https://www.jhauser.us/arithmetic/HardFloat-1/doc/HardFloat-Verilog.html)은 payload 전파를 specialization으로 다루며, signaling NaN을 quiet 형태로 바꾸는 표현을 설명한다.

[RISC-V F 명세의 NaN Generation and Propagation](https://github.com/riscv/riscv-isa-manual/blob/main/src/unpriv/f-st-ext.adoc)은 특별히 정한 경우를 제외한 NaN 결과를 canonical NaN으로 만들도록 정의한다. binary32 기본 pattern은 0x7FC00000이다. 따라서 입력 payload가 다르더라도 산술 출력은 같은 canonical 값이 될 수 있다.

예를 들어 quiet NaN 패턴 0x7FC00001을 정상값과 더한 결과에서 끝의 1이 사라지는 것은 이 정책 아래에서 오류가 아니다. 반대로 payload 보존을 요구하는 모드나 다른 구현의 reference에는 그 정보가 결과 선택에 들어갈 수 있다. “둘 다 NaN”이라는 비교와 bitwise 비교가 다른 검증인 이유다.

여러 입력이 NaN일 때 어느 payload를 고를지도 정책에 따라 달라질 수 있다. source의 지원 모드와 specialization을 확인하지 않고 한 우선순위를 모든 연산기에 강제하면 잘못된 mismatch를 만들 수 있다.

## 저장과 산술은 같은 전파 경로가 아니다

RISC-V의 부동소수점 load·store는 전달하는 bits를 바꾸지 않으며 non-canonical NaN payload도 보존한다. 이미 있는 NaN을 메모리에서 레지스터로 옮기는 일과 그 값을 산술 연산에 넣어 결과 NaN을 만드는 일의 규칙이 다르다.

따라서 메모리에서 읽은 0x7FC00001이 그대로 보인다는 테스트가 산술 payload 보존을 입증하지 않는다. 산술 후 canonical 값으로 바뀌었다고 load·store의 bit 보존 계약이 깨진 것도 아니다. 테스트의 관측 지점을 구분해야 한다.

컴파일러나 언어가 숫자 표현을 다루는 규칙은 별도로 확인해야 한다. 하드웨어 명령의 load·store 보장을 임의의 고수준 프로그램 복사에 그대로 적용할 수 있다는 뜻은 아니다.

## 비교의 false와 invalid도 구분하기

NaN은 일반적인 숫자 순서에 넣을 수 없다. RISC-V의 부동소수점 비교는 NaN 입력에서 false 결과를 내지만, flag 규칙은 비교 종류에 따라 다르다. FEQ는 signaling NaN에서 invalid를 설정하고, FLT와 FLE는 어떤 NaN이든 invalid를 설정한다.

이 차이를 무시하고 “NaN이면 결과 false”만 검사하면 숫자 출력은 맞지만 flag는 틀릴 수 있다. 반대로 quiet NaN을 다루는 equality 테스트에서 항상 invalid를 요구하면 정상 구현을 실패로 처리할 수 있다. 연산 이름과 입력 NaN 종류를 함께 검사해야 한다.

누산형 exception flag도 관측에 영향을 준다. 이전 명령의 invalid가 이미 남아 있다면 지금의 quiet NaN 연산 뒤에도 flag는 1로 보일 수 있다. 새 사건을 확인하려면 누적 flag의 초기 상태와 지우는 시점을 통제해야 한다.

## 검증에서 NaN을 전부 같게 보아도 될까

목적이 숫자 결과의 클래스 비교라면 모든 NaN을 같은 범주로 취급하는 검사가 유용할 수 있다. 하지만 payload, canonical 형식, signaling 처리나 flag를 검증하려면 별도의 조건이 필요하다. 하나의 NaN 허용 규칙으로 모든 정확성을 대신하지 않는다.

입력에는 quiet·signaling을 나누고 payload가 다른 두 NaN을 넣어 본다. NaN이 없는 invalid 사건도 검사하면 단순 전달과 생성 경로를 분리할 수 있다. 그 결과를 숫자 class, bit pattern, 새 flag와 누적 flag로 나누어 읽으면 실패 원인이 명확해진다.

NaN 전파는 비정상 값을 다음 단계로 보내는 단순 복사가 아니다. **어떤 사건을 알릴지, 어떤 표현을 선택할지, 어떤 flag를 남길지를 정하는 수치 인터페이스**다. 같은 NaN 출력 뒤의 서로 다른 사건을 구분해야 계산의 실패를 정확하게 추적할 수 있다.

