F·D·Q·Zfh: 부동소수점
RISC-V의 부동소수점은 새로 발명한 것이 아니다. IEEE 754-2008 표준을 그대로 따르고, 그 위에 레지스터 파일 f0–f31, 상태 레지스터 fcsr, 그리고 몇 가지 RISC-V다운 결정을 얹었다. 예외가 생겨도 트랩하지 않고 플래그만 세우며, NaN은 언제나 하나의 정해진 모양(정규 NaN)으로 만들고, 좁은 값을 넓은 레지스터에 넣을 때는 위쪽을 1로 채우는 NaN 박싱을 쓴다. 이 장에서는 비트 하나하나를 직접 눌러 보며 F·D·Q·Zfh와 BF16, Zfa, Zfinx까지 정리한다.
- binary16·bfloat16·binary32·binary64·binary128의 부호·지수·가수 배치를 읽고 값과 FCLASS 분류를 계산한다.
- f0–f31, FLEN, fcsr(frm 7:5, fflags 4:0)과 CSR 주소 0x001–0x003을 안다.
- RNE·RTZ·RDN·RUP·RMM 다섯 반올림 모드와 rm = 111(DYN)의 뜻을 구별한다.
- R4 형식의 FMA 네 명령, 부호 주입, 비교, 변환, FCLASS 명령을 정리한다.
- NV·DZ·OF·UF·NX 플래그가 언제 서는지, 정규 NaN 0x7fc00000이 무엇인지 안다.
- NaN 박싱 규칙과, 박싱이 깨진 값을 읽을 때 무슨 일이 일어나는지 설명한다.
- Zfh/Zfhmin, BF16(Zfbfmin·Zvfbfmin·Zvfbfwma), Zfa, Zfinx/Zdinx/Zhinx의 역할을 비교한다.
IEEE 754 형식: 부호, 지수, 가수
IEEE 754 이진 부동소수점 수는 세 부분으로 이루어진다. 부호 1비트, 치우침(bias)을 더해 저장하는 지수, 그리고 맨 앞의 1을 생략하고 저장하는 가수(fraction)다. 정규수의 값은 (−1)^s × 1.f × 2^(E − bias)다. 지수 비트가 모두 0이면 0 또는 비정규수(subnormal), 모두 1이면 무한대 또는 NaN이다. RISC-V의 다섯 가지 형식은 이 틀의 폭만 다르다.
IEEE 754 비트 편집기
f 레지스터와 fcsr
F 확장은 32개의 부동소수점 레지스터 f0–f31을 더한다. 정수 레지스터와 달리 f0도 평범한 레지스터다(늘 0이 아니다). 레지스터 폭을 FLEN이라 부르는데, F만 있으면 32, D가 있으면 64, Q가 있으면 128이다. 정수와 부동소수점 레지스터를 따로 둔 이유를 규격은 이렇게 설명한다. 같은 명령 폭으로 더 많은 레지스터를 쓸 수 있고, 슈퍼스칼라에서 레지스터 포트를 나눠 달기 쉽고, 부동소수점 유닛을 분리해 설계하거나 내부 재부호화(recoding)를 쓰기 쉽다. F 확장은 CSR 접근을 위해 Zicsr에 의존한다.
| CSR | 주소 | 내용 | 의사 명령 |
|---|---|---|---|
fflags | 0x001 | 누적 예외 플래그 (fcsr[4:0]) | frflags rd / fsflags rd, rs1 |
frm | 0x002 | 동적 반올림 모드 (fcsr[7:5]) | frrm rd / fsrm rd, rs1 |
fcsr | 0x003 | frm + fflags | frcsr rd / fscsr rd, rs1 |
f 레지스터 32개와 fcsr은 문맥 교환 때 저장해야 할 상태가 꽤 크다. 특권 규격의 mstatus.FS 필드가 부동소수점 상태가 Off·Initial·Clean·Dirty 중 무엇인지 추적해, 쓰지 않은 프로세스의 f 레지스터 저장을 건너뛸 수 있게 한다(17장).
다섯 반올림 모드
정확한 결과가 표현 가능한 두 값 사이에 떨어지면 하나를 골라야 한다. 반올림을 하는 명령에는 3비트 rm 필드가 있어서 명령마다 정적으로 모드를 정할 수 있고, rm = 111(DYN)이면 fcsr의 frm에 든 동적 모드를 쓴다.
| rm | 이름 | 뜻 |
|---|---|---|
000 | RNE | 가장 가까운 값, 정확히 중간이면 짝수(가수 최하위 비트 0) 쪽 — IEEE 기본값 |
001 | RTZ | 0 쪽으로 (버림) |
010 | RDN | −∞ 쪽으로 (내림) |
011 | RUP | +∞ 쪽으로 (올림) |
100 | RMM | 가장 가까운 값, 정확히 중간이면 크기가 큰 쪽 |
101, 110 | — | 예약 |
111 | DYN | 명령의 rm에서는 “frm을 써라”. frm 레지스터에 이 값을 넣는 것은 예약 |
반올림 모드 시각화 (binary32)
명령어 한눈에 보기
F 확장의 명령은 정수 쪽과 거의 같은 형식을 쓴다. load/store는 I형·S형과 같은 “기준 + 12비트 오프셋” 주소 방식이고, 연산은 OP-FP 주 opcode(1010011)의 R형이다. 예외는 세 피연산자를 읽는 FMA로, 이를 위해 R4 형식이 새로 만들어졌다.
| 무리 | 명령 (.S 기준, .D/.Q/.H 도 같은 꼴) | 요점 |
|---|---|---|
| load/store | flw · fsw (LOAD-FP 0000111 / STORE-FP 0100111) | 비트를 바꾸지 않는다(비정규 NaN 페이로드도 보존). width: H 001, W 010, D 011, Q 100 |
| 산술 | fadd · fsub · fmul · fdiv · fsqrt | rm 필드로 반올림 |
| 융합 곱셈-덧셈 | fmadd · fmsub · fnmsub · fnmadd | R4 형식, rs3, 반올림 1번 |
| 최소/최대 | fmin · fmax | −0 < +0. NaN 하나면 다른 쪽, 둘 다 NaN이면 정규 NaN |
| 부호 주입 | fsgnj · fsgnjn · fsgnjx | 의사 명령 fmv.s · fneg.s · fabs.s. 플래그 없음, NaN 정규화 없음 |
| 비교 | feq · flt · fle | 결과는 정수 레지스터에 0/1. flt/fle는 NaN이면 NV, feq는 sNaN일 때만 NV |
| 변환 | fcvt.w.s · fcvt.wu.s · fcvt.s.w · fcvt.s.wu (RV64: .l/.lu) | 범위 밖이면 가장 가까운 값으로 자르고 NV |
| 비트 이동 | fmv.x.w · fmv.w.x | 해석 없이 32비트 그대로. 옛 이름 fmv.x.s / fmv.s.x |
| 분류 | fclass.s | 정수 rd에 10비트 마스크, 정확히 한 비트만 1 |
| FCLASS rd 비트 | 뜻 | 비트 | 뜻 |
|---|---|---|---|
| 0 | −∞ | 5 | 양의 비정규수 |
| 1 | 음의 정규수 | 6 | 양의 정규수 |
| 2 | 음의 비정규수 | 7 | +∞ |
| 3 | −0 | 8 | 신호 NaN (sNaN) |
| 4 | +0 | 9 | 조용한 NaN (qNaN) |
비교(feq/flt/fle), 분류(fclass), 정수 변환(fcvt.w.s)은 결과를 곧장 정수 레지스터에 쓴다. 규격은 이것을 의도된 설계로 설명한다. 구현이 f 레지스터 안에서 값을 내부 형식으로 재부호화해 둘 수 있게 하려고 f 레지스터에 정수를 두지 않으며, 덕분에 레지스터 사이 이동 명령도 줄어든다. 플래그 대신 정수 비교 결과를 써서 분기하는 것도 같은 철학이다.
예외는 트랩이 아니라 플래그로, NaN은 정규 NaN으로
IEEE 754는 예외 상황(0으로 나누기 등)에서 트랩할지 기본값을 돌려줄지를 구현에 맡긴다. RISC-V는 트랩하지 않는다. 표준이 정한 기본 결과(무한대, NaN 등)를 돌려주고 fflags의 해당 비트만 세운다. 플래그를 직접 보고 분기하는 명령도 일부러 넣지 않았다. 필요하면 소프트웨어가 frflags로 읽어 확인한다.
| 플래그 | 서는 경우 (예) |
|---|---|
| NV | 0/0, ∞−∞, √(음수), sNaN 입력, 정수 변환 범위 밖·NaN, flt/fle에 NaN, ∞×0 (FMA에서는 덧셈항이 qNaN이어도) |
| DZ | 유한한 0 아닌 수 ÷ 0 → ±∞ |
| OF | 반올림한 결과의 크기가 가장 큰 유한값을 넘음 (항상 NX도 함께) |
| UF | 결과가 아주 작고(tiny) 부정확함. RISC-V는 tininess를 반올림 뒤에 판단 |
| NX | 반올림한 결과가 정확한 값과 다름 |
“Except when otherwise stated, if the result of a floating-point operation is NaN, it is the canonical NaN. The canonical NaN has a positive sign and all significand bits clear except the MSB, a.k.a. the quiet bit. For single-precision floating-point, this corresponds to the pattern 0x7fc00000.”
"F" Extension, NaN Generation and Propagation — 표준이 권장하는 NaN 페이로드 전파는 하드웨어 비용 때문에 하지 않는다. 표준에서도 선택 사항이라 이식성 있는 코드가 기댈 수 없기 때문이다.
정수 변환이 범위를 벗어나면 결과는 가장 가까운 표현 가능한 정수로 잘리고 NV가 선다. NaN은 “가장 큰 양수”로 간다는 점이 특이하다.
| FCVT.W.S | FCVT.WU.S | FCVT.L.S | FCVT.LU.S | |
|---|---|---|---|---|
| 유효 입력 최소 (반올림 후) | −231 | 0 | −263 | 0 |
| 유효 입력 최대 (반올림 후) | 231−1 | 232−1 | 263−1 | 264−1 |
| 범위 밖 음수, −∞ | −231 | 0 | −263 | 0 |
| 범위 밖 양수, +∞, NaN | 231−1 | 232−1 | 263−1 | 264−1 |
fcsr 플래그 놀이터
flt와 feq에 같은 qNaN을 넣으면 앞의 것만 NV를 세운다. 플래그는 지우기 전까지 남아 있다(sticky).F v2.2부터 FMIN.S·FMAX.S는 IEEE 754-2008의 minNum/maxNum이 아니라 당시 제안된 754-201x의 minimumNumber/maximumNumber를 따른다. 둘은 신호 NaN 처리에서 다르다. 지금 규칙은 “NaN이 하나면 다른 쪽 값, 둘 다 NaN이면 정규 NaN, sNaN 입력은 결과가 NaN이 아니어도 NV”다. NaN이 하나라도 있으면 NaN을 내는 IEEE 754-2019 minimum/maximum은 Zfa의 FMINM/FMAXM이 제공한다.
D 확장과 NaN 박싱
D 확장은 f 레지스터를 64비트(FLEN = 64)로 넓히고 배정도 명령을 더한다. 이제 한 레지스터에 단정도 값과 배정도 값이 모두 들어갈 수 있다. 그렇다면 32비트 값을 64비트 레지스터에 넣을 때 위쪽 32비트는 무엇으로 채울까? RISC-V의 답은 NaN 박싱(NaN-boxing)이다. 위쪽 비트를 모두 1로 채운다.
- 전송 명령(FLn/FSn, FMV.n.X/FMV.X.n): n비트 값을 f 레지스터로 들일 때는 올바르게 박싱한다. 내보낼 때는 아래 n비트만 쓰고 위쪽은 무시한다.
- 그 밖의 모든 n비트 연산: 입력의 위쪽 FLEN−n비트가 모두 1인지 검사한다. 맞으면 아래 n비트를 값으로 쓰고, 아니면 입력을 n비트 정규 NaN으로 취급한다.
- n비트 결과를 f 레지스터에 쓰는 연산은 반드시 위쪽을 모두 1로 채운다.
“The upper bits of a valid NaN-boxed value must be all 1s. Valid NaN-boxed n-bit values therefore appear as negative quiet NaNs (qNaNs) when viewed as any wider m-bit value, n < m ≤ FLEN.”
"D" Extension, NaN Boxing of Narrower Values — 예전 규격은 이 경우를 정하지 않았다. 지금 규칙은 구현마다 다른 동작을 없애면서도, 값을 내부 형식으로 재부호화하는 구현과 그렇지 않은 구현을 모두 수용한다.
NaN 박싱 따라가기 (FLEN = 64)
D 확장의 나머지는 F와 거의 대칭이다. fcvt.s.d는 rm에 따라 반올림하고, fcvt.d.s는 언제나 정확하다. fcvt.d.w[u]도 늘 정확하다. 정수 레지스터와 64비트 비트 패턴을 주고받는 fmv.x.d·fmv.d.x는 XLEN ≥ 64(RV64)에서만 있다. RV32에서는 메모리를 거치거나, Zfa의 fmvh.x.d·fmvp.d.x를 쓴다. FLD·FSD는 주소가 자연 정렬되어 있고 XLEN ≥ 64일 때만 원자적으로 실행된다고 보장된다.
Q, Zfh/Zfhmin, 그리고 BF16
Q(v2.2)는 128비트 4배 정도를 더한다(FLEN = 128, D에 의존). 새 load/store flq/fsq는 width 100을 쓰고, RV32·RV64 어디에도 fmv.x.q/fmv.q.x는 없어서 정수 레지스터로 옮기려면 메모리를 거친다. Zfh(v1.0)는 16비트 반정도 연산 전부를, Zfhmin은 그중 저장과 변환만(flh, fsh, fmv.x.h, fmv.h.x, fcvt.s.h, fcvt.h.s, D가 있으면 fcvt.d.h, fcvt.h.d, Q가 있으면 fcvt.q.h, fcvt.h.q) 담는다. 둘 다 F에 의존한다. Zfhmin 소프트웨어는 반정도를 “저장 형식”으로만 쓰고 계산은 단정도로 하는 경우를 겨냥한다.
BF16(v1.0) 규격은 세 확장으로 나뉜다. 모두 F에 의존하며, 이 규격의 BF16 명령은 비정규수를 완전히 지원한다.
| 확장 | 명령 | 의존 |
|---|---|---|
| Zfbfmin | fcvt.bf16.s, fcvt.s.bf16 + Zfh의 flh, fsh, fmv.x.h, fmv.h.x (6개) | F |
| Zvfbfmin | vfncvtbf16.f.f.w, vfwcvtbf16.f.f.v (벡터 변환) | Zve32f (또는 V) |
| Zvfbfwma | vfwmaccbf16 (BF16 × BF16을 FP32에 누적하는 넓히기 곱셈-덧셈) | Zvfbfmin, Zfbfmin |
BF16 값도 f 레지스터에서는 반정도와 똑같이 NaN 박싱된다. 변환은 BF16 ↔ FP32만 제공한다. 규격은 이 확장들이 “BF16을 FP32의 정밀도를 줄인 형태로 쓰는” 용도를 겨냥한다고 밝히며, 다른 형식과의 변환은 FP32를 거쳐 흉내 낼 수 있다고 설명한다. 벡터 쪽 이야기는 15장에서 이어진다.
Zfa: 자주 쓰는 연산을 한 명령으로
Zfa(v1.0)는 F에 의존하며, 라이브러리와 컴파일러가 여러 명령으로 흉내 내던 연산을 더한다. 가장 눈에 띄는 것은 fli(load immediate)다. 상수 하나를 쓰려고 메모리에서 읽거나 정수 레지스터를 거칠 필요 없이, rs1 필드의 5비트로 32개 상수 중 하나를 고른다.
fli.s 상수표(rs1 = 0–31). 1번은 “가장 작은 양의 정규수”로 형식마다 값이 다르다(binary32: 2−126). 30·31번은 +∞와 정규 NaN이다. 8–22번은 규칙적인 무늬를 이루고, 어떤 항목도 가수의 위쪽 두 비트 밖을 쓰지 않는다. fli.h에서는 216을 표현할 수 없어 29번이 +∞가 된다. fli는 어떤 플래그도 세우지 않는다.| 명령 | 하는 일 | 인코딩 단서 |
|---|---|---|
fli.{s|d|h|q} | 32개 상수 중 하나를 rd에 | FMV.W.X 꼴, rs2 = 1 |
fminm · fmaxm | IEEE 754-2019 minimum/maximum: NaN이 하나라도 있으면 정규 NaN | FMIN/FMAX와 같고 비트 13 = 1 |
fround · froundnx | rm에 따라 정수로 반올림(결과는 부동소수점). nx 버전은 값이 바뀌면 NX | FCVT.S.D 꼴, rs2 = 4 / 5 |
fcvtmod.w.d | 늘 RTZ, 결과의 아래 32비트를 취해(모듈러) 부호 확장. ±∞·NaN → 0. JavaScript의 정수 변환 가속용 | FCVT.W.D 꼴, rs2 = 8, rm = 001 |
fleq · fltq | FLE/FLT와 같지만 조용한 NaN에는 NV를 세우지 않음 | FLE/FLT와 같고 비트 14 = 1 |
fmvh.x.d · fmvp.d.x | RV32+D: 배정도의 위쪽 32비트 읽기 / 정수 레지스터 두 개로 배정도 만들기 | funct7 1110001 / 1011001 |
fmvh.x.q · fmvp.q.x | RV64+Q: 같은 일을 128비트에 대해 | funct7 1110011 / 1011011 |
Zfinx: f 레지스터 없는 부동소수점
작은 마이크로컨트롤러에게 32개짜리 레지스터 파일을 하나 더 두는 것은 큰 비용이다. Zfinx(v1.0, “z-f-in-x”)는 F의 연산 명령을 그대로 두되, f 레지스터를 읽고 쓰던 자리에서 같은 번호의 x 레지스터를 쓴다. f 레지스터가 없으니 전송 명령 flw, fsw, fmv.w.x, fmv.x.w, c.flw[sp], c.fsw[sp]는 빠진다. 메모리와는 정수 load/store로, 레지스터 사이는 정수 연산이나 부호 주입 명령으로 옮긴다. 문맥 교환 비용도 줄어든다.
| 확장 | 내용 | 빠지는 전송 명령 |
|---|---|---|
| Zfinx | F의 연산을 x 레지스터에서 (Zicsr 의존) | FLW, FSW, FMV.W.X, FMV.X.W, C.FLW[SP], C.FSW[SP] |
| Zdinx | D의 연산 (Zfinx 의존). RV32는 짝수 레지스터 쌍, 아래 번호가 하위 비트 | FLD, FSD, FMV.D.X, FMV.X.D, C.FLD[SP], C.FSD[SP] |
| Zhinx | Zfh의 연산 (Zfinx 의존) | FLH, FSH, FMV.H.X, FMV.X.H |
| Zhinxmin | FCVT.S.H, FCVT.H.S (Zdinx가 있으면 FCVT.D.H, FCVT.H.D) | — |
RV32 Zdinx에서 배정도 결과를 x0에 쓰면 쓰기 전체가 무시되어 x1이 바뀌지 않고, x0을 배정도 피연산자로 쓰면 (x1을 읽지 않고) 전체가 0이다. 홀수 번호 레지스터를 배정도 피연산자로 쓰는 인코딩은 예약되어 있다.
핵심 정리
- RISC-V 부동소수점은 IEEE 754-2008을 따른다. F(v2.2)는 f0–f31(FLEN = 32)과 fcsr을, D·Q(v2.2)는 FLEN을 64·128로 넓힌다. fmt 필드는 S 00, D 01, H 10, Q 11.
- fcsr = frm[7:5] + fflags[4:0](NV DZ OF UF NX). CSR 주소는 fflags 0x001, frm 0x002, fcsr 0x003.
- rm: RNE 000, RTZ 001, RDN 010, RUP 011, RMM 100, 101·110 예약, 111 DYN(frm 사용).
- FMA 네 명령은 rs3를 가진 R4 형식을 쓰고 반올림을 한 번만 한다. 비교·분류·정수 변환은 결과를 정수 레지스터에 쓴다. FCLASS는 10비트 중 한 비트를 세운다.
- 예외는 트랩하지 않고 누적 플래그만 세운다. NaN 결과는 늘 정규 NaN(binary32: 0x7fc00000)이며 페이로드를 전파하지 않는다. tininess는 반올림 뒤에 판단한다.
- NaN 박싱: 좁은 값을 넣을 때 위쪽을 모두 1로 채우고, 좁은 연산은 박싱이 깨진 입력을 정규 NaN으로 취급한다.
- Zfh/Zfhmin은 반정도, BF16(Zfbfmin·Zvfbfmin·Zvfbfwma)은 bfloat16 변환과 벡터 누적, Zfa는 fli·fminm·fround·fcvtmod·fleq 등, Zfinx/Zdinx/Zhinx는 f 레지스터 없이 x 레지스터에서 부동소수점을 계산한다.
확인 퀴즈
fcsr의 frm 필드 위치와 frm CSR의 주소로 옳은 것은?
단정도 정규 NaN의 비트 패턴은?
RV64D 하트에서 flw f1로 1.0f(0x3f800000)를 읽은 뒤 f1의 64비트 값은?
16777217을 binary32로 변환할 때 RNE와 RMM의 결과로 옳은 것은?
RISC-V에서 1.0 / 0.0을 실행하면?
FMADD.S가 FMUL.S + FADD.S 두 명령과 다른 점은?
feq.s와 flt.s에 같은 조용한 NaN을 넣었을 때 NV 플래그는?
Zfinx에 대한 설명으로 틀린 것은?
더 읽을거리
- "F" Extension for Single-Precision Floating-Point, Version 2.2
- "D" Extension for Double-Precision Floating-Point, Version 2.2 · "Q" Extension, Version 2.2
- "Zfh" and "Zfhmin" Extensions for Half-Precision Floating-Point, Version 1.0
- "BF16" Extensions for BFloat16-precision Floating-Point, Version 1.0
- "Zfa" Extension for Additional Floating-Point Instructions, Version 1.0
- "Zfinx", "Zdinx", "Zhinx", "Zhinxmin" Extensions, Version 1.0
- IEEE Std 754-2008 / 754-2019, IEEE Standard for Floating-Point Arithmetic
- David Goldberg, “What Every Computer Scientist Should Know About Floating-Point Arithmetic” (ACM Computing Surveys, 1991)