C와 Zc*: 압축 명령어
RISC-V의 기본 명령은 모두 32비트다. 그런데 프로그램을 들여다보면 addi sp, sp, -16, mv a0, s0, ret처럼 정보가 몇 비트 안 되는 명령이 절반을 넘는다. C 확장은 이런 명령에 16비트짜리 짧은 이름을 붙여 코드 크기를 25–30% 줄인다. 핵심은 “새 명령을 만들지 않는다”는 것이다. 모든 16비트 명령은 이미 있는 32비트 명령 하나로 정확히 펼쳐진다. 이 장에서는 그 약속이 어떤 비트 배치로 실현되는지, 그리고 C를 잘게 나누고 늘린 Zc* 확장이 무엇을 더했는지 직접 압축해 보며 익힌다.
- 16비트 명령과 32비트 명령이 섞여 있을 때 하드웨어가 길이를 어떻게 알아내는지 설명한다.
- CR·CI·CSS·CIW·CL·CS·CA·CB·CJ 아홉 형식의 비트 배치와 쓰임새를 구별한다.
- 3비트 레지스터 칸이 왜 x8–x15를 가리키는지, 즉시값이 왜 4배·16배로 늘어나는지 안다.
- 주어진 32비트 명령이 압축될 수 있는지 판단하고, 안 된다면 어떤 조건 때문인지 짚는다.
- Zca·Zcf·Zcd·Zcb·Zcmp·Zcmt의 관계와 Zcmp·Zcmt가 Zcd와 함께 쓰일 수 없는 이유를 안다.
- IALIGN=16이 명령어 인출 하드웨어에 남기는 숙제(경계에 걸친 명령, 페이지 폴트)를 이해한다.
왜 16비트 명령인가
32비트 고정 길이는 해독기를 단순하게 만든다(4장). 대가는 코드 크기다. ret은 “ra로 점프”라는 정보뿐인데도 32비트를 다 차지한다. 코드가 커지면 마이크로컨트롤러는 플래시 메모리를 더 사야 하고, 큰 서버 프로그램은 명령어 캐시가 넘쳐 느려진다. RISC-V는 이 문제를 압축 명령어(compressed instructions, RVC)로 푼다. 자주 쓰이고 정보가 적은 명령에 한해 16비트 인코딩을 하나 더 정의하는 것이다.
lui t1, 0x12345는 즉시값이 커서, slt는 대응하는 16비트 명령이 없어서 32비트로 남는다. 16비트와 32비트 명령은 자유롭게 섞이며, 32비트 명령도 2바이트 경계(예: 4번지가 아닌 2·6번지)에서 시작할 수 있다.“Typically, 50%-60% of the RISC-V instructions in a program can be replaced with RVC instructions, resulting in a 25%-30% code-size reduction.”
Volume I, "C" Extension for Compressed Instructions — 같은 장의 비고는 Waterman의 측정을 인용해, RVC가 인출하는 명령어 비트를 25–30% 줄이고 명령어 캐시 미스를 20–25% 줄인다고, 이는 명령어 캐시를 두 배로 키운 것과 비슷한 효과라고 적는다.
압축 명령어는 RISC-V가 처음 생각한 아이디어가 아니다. 그러나 앞선 RISC들은 처음에 opcode 자리를 남겨 두지 않았기 때문에, 압축 명령을 별도의 ISA로 만들어야 했다. RISC-V는 “25년 뒤늦게 본 덕분에” 처음부터 자리를 비워 두었다.
| 방식 | 예 | 특징 | 결과 |
|---|---|---|---|
| 16비트 전용 모드 | ARM Thumb, MIPS16 | 모드를 바꿔 16비트 명령만 실행 | 코드는 작지만 실행 명령 수가 늘어 느려짐 |
| 16/32비트 혼합 (별도 ISA) | Thumb-2, microMIPS | 섞어 쓸 수 있지만 원래 ISA와 인코딩이 다름 | 성능은 회복, 대신 컴파일러·문서·구현이 세대마다 따로 |
| 처음부터 계획한 확장 | RISC-V C | 기본 ISA 위에 얹는 확장, 언제든 섞어 씀, 16비트 하나 = 32비트 하나 | 도구와 하드웨어 변경이 최소 |
섞여 있어도 길이를 아는 법: 사분면과 IALIGN
16비트와 32비트 명령이 아무 순서로나 섞여 있으면, 하드웨어는 명령이 어디서 끝나는지 어떻게 알까? 답은 가장 아래 두 비트다. RISC-V의 모든 32비트 명령은 inst[1:0] = 11로 끝난다(4장의 opcode가 모두 …11인 이유). 따라서 아래 두 비트가 00, 01, 10이면 16비트 명령이다. RVC는 이 세 값을 사분면(quadrant) 0·1·2라 부르고, 각 사분면 안에서 inst[15:13](funct3)로 명령을 다시 나눈다.
JAL/ADDIW)이 있다.IALIGN = 16: 명령이 2바이트 경계에서 시작한다
C 확장이 없을 때 명령은 4바이트 경계에만 놓인다(IALIGN=32). C가 있으면 32비트 명령도 2바이트 경계에서 시작할 수 있다(IALIGN=16). 규격은 이렇게 하면 32비트 명령 앞에 2바이트 빈칸(패딩)을 넣지 않아도 되므로 코드 밀도가 크게 좋아진다고 설명한다. 덤으로, C가 있는 시스템에서는 분기·점프 목표 주소가 짝수이기만 하면 되므로 명령 주소 정렬 예외(instruction-address-misaligned)가 아예 일어날 수 없다.
규칙 하나: 16비트 명령은 32비트 명령 하나로 펼쳐진다
RVC의 가장 중요한 설계 제약은 “모든 RVC 명령은 기본 ISA(또는 F·D)의 32비트 명령 정확히 하나로 펼쳐진다”이다. 그래서 하드웨어는 해독 단계 앞에 작은 확장기(expander, decompressor)만 붙이면 되고, 그 뒤의 파이프라인(8장)은 C가 있는지조차 모른다. 컴파일러도 C를 몰라도 된다. 어셈블러와 링커가 알아서 줄여 준다.
규격은 0x0000을 “정의된 불법 명령”으로 영구히 예약했다. 지워진 메모리나 존재하지 않는 영역(대개 0으로 채워진다)으로 잘못 뛰어들었을 때 곧바로 트랩이 나도록 하기 위해서다. 같은 이유로 모든 비트가 1인 값도 불법 명령으로 남겨 두었다. 비표준 확장도 이 값들을 다른 뜻으로 쓰면 안 된다.
아홉 가지 압축 형식
16비트에 opcode·레지스터·즉시값을 다 넣으려면 무언가를 포기해야 한다. RVC는 세 가지 방법을 섞는다. 첫째, 목적지와 첫 원본을 같게 한다(add rd, rd, rs2처럼 2-주소 형식). 둘째, 레지스터를 3비트로 줄여 가장 많이 쓰는 8개만 가리킨다. 셋째, 즉시값을 짧게 하고 자료 크기만큼 배율을 곱한다. 이 조합에 따라 형식이 아홉 가지로 나뉜다.
| 형식 | 뜻 | 레지스터 | 대표 명령 |
|---|---|---|---|
| CR | Register | 5비트 rd/rs1, rs2 | c.mv c.add c.jr c.jalr c.ebreak |
| CI | Immediate | 5비트 rd/rs1 | c.li c.lui c.addi c.addi16sp c.slli c.lwsp |
| CSS | Stack-relative Store | 5비트 rs2 | c.swsp |
| CIW | Wide Immediate | 3비트 rd′ | c.addi4spn (8비트 즉시값) |
| CL | Load | 3비트 rd′, rs1′ | c.lw (c.flw c.fld c.ld) |
| CS | Store | 3비트 rs1′, rs2′ | c.sw (c.fsw c.fsd c.sd) |
| CA | Arithmetic | 3비트 rd′/rs1′, rs2′ | c.sub c.xor c.or c.and |
| CB | Branch/Arithmetic | 3비트 rs1′ | c.beqz c.bnez c.srli c.srai c.andi |
| CJ | Jump | 없음 | c.j c.jal |
3비트 레지스터는 왜 x8–x15인가
CIW·CL·CS·CA·CB 형식의 레지스터 칸(rd′·rs1′·rs2′, 프라임 기호를 붙여 구별한다)은 3비트라 8개밖에 못 가리킨다. 규격은 이 8개를 x8–x15로 정했고, 반대로 ABI 쪽에서 가장 자주 쓰는 레지스터들(s0·s1과 인자 레지스터 a0–a5)이 바로 이 번호에 오도록 호출 규약을 바꿨다(7장). 연속하고 자연스럽게 정렬된 번호 묶음이라 해독기는 3비트 앞에 01만 붙이면 된다. 레지스터가 16개뿐인 RV32E/RV64E와도 그대로 맞는다.
t0–t6나 s2–s11을 쓰는 연산은 CA·CL·CS 형식으로 줄일 수 없다. 컴파일러가 뜨거운 값을 s0·s1·a0–a5에 배치할수록 코드가 작아지는 이유다.C 명령어 한눈에 보기
RV32C의 정수 명령을 펼쳐지는 32비트 명령과 함께 정리하면 다음과 같다. “조건”을 벗어나면 그 16비트 인코딩은 다른 명령의 자리이거나, 예약되었거나, HINT(아무 상태도 바꾸지 않는 성능 힌트용 자리)다.
| C 명령 | 형식 | 펼친 32비트 명령 | 조건 · 범위 |
|---|---|---|---|
| c.addi4spn | CIW | addi rd′, sp, nzuimm | 4–1020, 4의 배수 (0은 예약) |
| c.lw | CL | lw rd′, uimm(rs1′) | 0–124, 4의 배수 |
| c.sw | CS | sw rs2′, uimm(rs1′) | 0–124, 4의 배수 |
| c.nop | CI | addi x0, x0, 0 | 즉시값 ≠ 0 이면 HINT |
| c.addi | CI | addi rd, rd, nzimm | rd ≠ x0, −32–31, 0은 HINT |
| c.jal | CJ | jal ra, offset | RV32 전용, ±2 KiB |
| c.li | CI | addi rd, x0, imm | −32–31, rd = x0 이면 HINT |
| c.addi16sp | CI | addi sp, sp, nzimm | 16의 배수, −512–496, 0은 예약 |
| c.lui | CI | lui rd, nzimm | rd ≠ x0·x2, nzimm[17:12] 6비트 부호 값 ≠ 0 |
| c.srli · c.srai | CB | srli/srai rd′, rd′, shamt | shamt ≠ 0 (RV32는 shamt[5] = 0) |
| c.andi | CB | andi rd′, rd′, imm | −32–31 |
| c.sub · c.xor · c.or · c.and | CA | op rd′, rd′, rs2′ | 세 레지스터 모두 x8–x15 |
| c.j | CJ | jal x0, offset | ±2 KiB |
| c.beqz · c.bnez | CB | beq/bne rs1′, x0, offset | ±256 B |
| c.slli | CI | slli rd, rd, shamt | rd = x0 이나 shamt = 0 은 HINT |
| c.lwsp | CI | lw rd, uimm(sp) | rd ≠ x0, 0–252, 4의 배수 |
| c.swsp | CSS | sw rs2, uimm(sp) | 0–252, 4의 배수 |
| c.jr | CR | jalr x0, 0(rs1) | rs1 ≠ x0 (ret = c.jr ra) |
| c.jalr | CR | jalr ra, 0(rs1) | rs1 ≠ x0, 링크 값은 pc + 2 |
| c.mv | CR | add rd, x0, rs2 | rs2 ≠ x0 (0이면 c.jr 자리) |
| c.add | CR | add rd, rd, rs2 | rs2 ≠ x0 (0이면 c.jalr·c.ebreak 자리) |
| c.ebreak | CR | ebreak | c.add 자리에서 rd = rs2 = 0 |
표에는 정수 명령만 넣었다. F·D 확장이 함께 있으면 c.flw·c.fsw·c.flwsp·c.fswsp(RV32만)와 c.fld·c.fsd·c.fldsp·c.fsdsp도 반드시 제공해야 한다. RV64C는 같은 자리를 c.ld·c.sd·c.ldsp·c.sdsp(64비트 적재·저장)와 c.addiw(RV32의 c.jal 자리), c.addw·c.subw에 쓴다. 주소 공간이 넓은 RV64에서는 64비트 값 적재·저장이 더 중요하고, 작은 RV32 마이크로컨트롤러에서는 짧은 함수 호출이 더 흔하기 때문이다.
즉시값은 흩어져 있고, 배율이 곱해진다
적재·저장 오프셋은 부호 없는 값이고 자료 크기만큼 배율이 붙는다(워드 ×4, 더블워드 ×8). 정렬된 워드의 주소 하위 두 비트는 어차피 0이니 저장할 필요가 없다. 그 대신 같은 5비트로 4배 먼 곳까지 닿는다. 32비트 명령처럼 즉시값 비트는 흩어져 있는데, 규격은 “되도록 많은 비트가 모든 명령에서 같은 자리에 오도록” 하기 위해서라고 설명한다. 부호 확장이 필요한 즉시값의 부호 비트는 언제나 inst[12]다.
c.lw a0, 36(a1) = 0x51c8을 해독하는 과정. inst[12:10]이 uimm[5:3], inst[6]이 uimm[2], inst[5]가 uimm[6]이 되고 하위 두 비트 0이 붙어 오프셋 36이 된다. 금색 점선이 비트의 이동 경로다.| 명령 | 저장 비트 | 배율 | 범위 | 왜 이 범위인가 |
|---|---|---|---|---|
c.lw / c.sw | 5 | ×4 | 0–124 | 구조체 필드·작은 배열 |
c.lwsp / c.swsp | 6 | ×4 | 0–252 | 스택 프레임 안의 저장 슬롯 |
c.addi16sp | 6 (부호) | ×16 | −512–496 | ABI가 sp를 늘 16바이트 정렬로 유지 |
c.addi4spn | 8 | ×4 | 4–1020 | 스택에 있는 지역 변수의 주소 만들기 |
c.beqz / c.bnez | 8 (부호) | ×2 | ±256 B | 짧은 루프와 조건문 |
c.j / c.jal | 11 (부호) | ×2 | ±2 KiB | 같은 함수 안, 가까운 함수 호출 |
c.addi a0, 0처럼 아무 효과가 없는 인코딩은 “하는 일 없는 계산”으로 실행해도 되므로, 단순한 구현은 신경 쓸 필요가 없다. 규격은 이런 자리를 성능 힌트용으로 모아 두었다. 실제로 c.add x0, x2–c.add x0, x5 네 자리는 Zihintntl의 c.ntl.*(이번 접근은 다시 안 쓸 데이터라는 힌트)로 쓰인다. RV32C의 HINT 공간 중 78%는 표준용, 나머지는 사용자 정의용이다.
직접 압축해 보기
아래 압축기에 RV32I 명령 하나를 넣으면, 그 명령이 될 수 있는 C 명령 후보를 모두 검사해 성공한 인코딩의 비트 배치를 보여 주고, 실패한 후보는 어떤 조건 때문에 안 되는지 알려 준다. 어셈블러가 실제로 하는 일과 같다. 이 압축기의 결과는 LLVM 어셈블러(llvm-mc)의 인코딩과 한 비트까지 같도록 검증해 두었다.
RV32I → RVC 압축기
lw a5, 8(a0)와 lw t3, 8(a0), lw a5, 128(a0)와 lw a5, 128(sp)를 차례로 넣어 보자. 레지스터 하나, 기준 주소 하나만 바뀌어도 결과가 달라진다. mv a0, a1은 addi인데도 c.mv(= add)가 되는 점도 눈여겨보자. 결과가 같으면 다른 32비트 명령으로 펼쳐져도 된다.코드 밀도 실험
명령 하나하나보다 중요한 것은 프로그램 전체가 얼마나 줄어드느냐다. 아래 실험은 riscv.js로 프로그램을 어셈블한 뒤 각 명령을 압축기에 넣는다. 분기·점프는 조금 까다롭다. 앞쪽 명령이 줄면 목표까지의 거리도 줄기 때문에, 처음에는 범위를 벗어나던 분기가 압축 가능해질 수 있다. 그래서 실제 링커처럼 배치 → 거리 재계산 → 다시 압축을 더 바뀌지 않을 때까지 되풀이한다(링커 완화, linker relaxation).
메모리 배치가 줄어드는 모습
t0를 a5로 바꾸면 몇 바이트가 줄어드는지 소스를 고쳐 확인해 보자.16비트 명령은 크기가 절반이므로, 명령의 50–60%가 압축되면 코드 크기는 25–30% 준다. 규격 서두의 숫자가 바로 이 계산이다. 압축률은 컴파일러의 레지스터 배치에 크게 좌우된다. 그래서 컴파일러의 레지스터 할당기는 C가 켜져 있으면 압축하기 좋은 레지스터(x8–x15)를 고르도록 힌트를 주기도 한다(예: LLVM).
Zc*: C를 쪼개고, 늘리고
C 확장은 한 덩어리로 정의되었지만, 쓰다 보니 두 가지 요구가 생겼다. 하나는 나누기다. 부동소수점 압축 적재·저장은 F·D가 있을 때만 의미가 있으니 따로 부를 이름이 필요했다. 다른 하나는 늘리기다. 마이크로컨트롤러 업계는 코드 크기를 더 줄일 명령을 원했다. 그 결과가 Zc* 확장 묶음(v1.0.0)이다.
c.fsdsp 인코딩 자리를 다시 쓰므로, C와 D를 함께 가진(따라서 Zcd를 가진) 구현에서는 쓸 수 없다.| 확장 | 내용 | 의존 · 주의 |
|---|---|---|
| Zca | C에서 부동소수점 적재·저장을 뺀 모든 명령 | C는 항상 Zca를 포함 |
| Zcf | c.flw c.flwsp c.fsw c.fswsp | RV32 전용, Zca·F 필요. C+F이면 자동 포함 |
| Zcd | c.fld c.fldsp c.fsd c.fsdsp | Zca·D 필요. C+D이면 자동 포함 |
| Zcb | 바이트·하프워드 적재·저장, 확장·반전, 곱셈의 16비트판 | Zca 필요. 인코딩 충돌 없음 |
| Zcmp | 함수 프롤로그·에필로그용 push/pop, 레지스터 쌍 이동 | Zca 필요. Zcd와 양립 불가, 응용 프로세서 프로파일과 맞지 않음 |
| Zcmt | 표를 거치는 점프(table jump) | Zca·Zicsr 필요. Zcd와 양립 불가, RVA 프로파일과 맞지 않음 |
“This extension reuses some encodings from c.fsdsp. Therefore it is incompatible with Zcd, which is included when C and D extensions are both present.”
Volume I, "Zc*" Extension for Code Size Reduction — Zcmp 절과 Zcmt 절에 똑같은 문장이 있다. 실제로 LLVM 어셈블러에 Zcmp와 Zcd를 함께 켜면 “incompatible” 오류를 내며 거부한다.
Zcb: 기본 C가 빠뜨린 것들
기본 C는 워드 단위 적재·저장만 줄였다. 문자열과 바이트 배열을 많이 다루는 임베디드 코드에서는 아쉬운 부분이다. Zcb는 비어 있던 사분면 0의 자리(funct3 = 100)와 CA 형식의 남는 자리를 써서 다음 명령을 더한다. 모두 기존 32비트 명령의 16비트판이다.
| Zcb 명령 | 같은 일을 하는 32비트 명령 | 조건 | 함께 필요한 확장 |
|---|---|---|---|
| c.lbu | lbu rd′, uimm(rs1′) | uimm 0–3 | — |
| c.lhu · c.lh | lhu/lh rd′, uimm(rs1′) | uimm 0 또는 2 | — |
| c.sb | sb rs2′, uimm(rs1′) | uimm 0–3 | — |
| c.sh | sh rs2′, uimm(rs1′) | uimm 0 또는 2 | — |
| c.zext.b | andi rd′, rd′, 0xff | rd = rs1 | — |
| c.sext.b · c.zext.h · c.sext.h | sext.b · zext.h · sext.h | rd = rs1 | Zbb (14장) |
| c.zext.w | add.uw rd′, rd′, zero | RV64 전용 | Zba |
| c.not | xori rd′, rd′, -1 | rd = rs1 | — |
| c.mul | mul rd′, rd′, rs2′ | CA 형식 | M 또는 Zmmul (9장) |
Zcmp: 프롤로그·에필로그를 한 명령으로
함수 앞뒤에서 ra와 s 레지스터를 스택에 저장하고 되살리는 코드는 정적 코드 크기의 큰 몫을 차지한다. 기본 C를 설계할 때는 load-multiple/store-multiple을 일부러 뺐다. 가상 메모리에서 일부만 실행된 명령을 다시 시작하는 장치가 필요하고, 대응하는 32비트 명령도 없기 때문이다. 대신 공용 저장·복원 루틴을 호출하는 소프트웨어 방법(GCC의 -msave-restore)을 권했다. Zcmp는 임베디드용으로 그 일을 하드웨어 명령으로 되살렸다.
processMarkers)의 프롤로그·에필로그는 60바이트에서, 공용 루틴 호출로 14바이트, push/pop으로 4바이트가 된다. 오른쪽: cm.push는 목록의 레지스터를 이전 sp 바로 아래부터 차례로 저장하고(s2가 −4, ra가 −16), 레지스터 몫(16 B)에 spimm×16을 더한 만큼 sp를 내린다.cm.popret은 복원 후 ret까지, cm.popretz는 a0에 0을 넣고 돌아가는 일까지 한 명령으로 한다. 레지스터 목록은 {ra}, {ra, s0}, … {ra, s0-s9}, {ra, s0-s11}의 12가지이고, {ra, s0-s10}은 인코딩 공간을 아끼려고 뺐다. cm.mvsa01(a0·a1 → s 레지스터 두 개)과 cm.mva01s(s 레지스터 두 개 → a0·a1)는 인자를 저장 레지스터로 옮기는 흔한 두 줄을 한 줄로 만든다. 이 명령들은 여러 메모리 접근을 하므로, 도중에 트랩이 나면 처음부터 다시 실행된다. 그래서 sp가 가리키는 메모리는 여러 번 써도 안전한(idempotent) 메모리여야 한다.
Zcmt: 함수 주소를 표로 압축
멀리 있는 함수를 부르려면 auipc + jalr 8바이트가 필요하다. 같은 함수를 여러 곳에서 부른다면, 그 주소를 표에 한 번만 적어 두고 호출은 “표의 몇 번째”로만 하면 된다. 일종의 사전 압축이다. Zcmt의 cm.jt index(index 0–31, 링크 없음)와 cm.jalt index(index 32–255, ra ← pc + 2)가 그 일을 하고, 표의 위치는 새 CSR jvt(주소 0x017, 사용자 모드에서 읽고 쓸 수 있음)가 가리킨다.
cm.jalt 40의 동작. RV32에서는 jvt.base + index × 4 번지(RV64는 × 8)의 표 항목을 읽어 그 주소(비트 0은 지움)로 점프한다. 표 항목 읽기는 명령어 인출의 연장으로 취급되어 실행 권한이 필요하고, 표를 고친 뒤에는 fence.i가 있어야 인출 쪽에 보인다. 표의 주소 값은 그림을 위한 예시다.misa의 C 비트(17장)는 “C 확장 전체”를 뜻한다. 그래서 Zca만 있고 F가 없거나, RV32에서 Zca·Zcf·F가 있거나, Zca·Zcd·D가 있을 때 켜진다. Zcmp·Zcmt를 쓰는 구현은 Zcd를 가질 수 없으므로, D가 있는 구현에서는 C 비트를 켤 수 없다는 점에 주의하자.
구현의 숙제: 경계에 걸친 명령어
IALIGN=16은 코드 밀도에는 좋지만 명령어 인출 하드웨어에는 숙제를 남긴다. 인출 장치는 보통 4·8·16바이트처럼 정렬된 덩어리(인출 블록, 캐시 라인)를 한 번에 읽는다. 32비트 명령이 2바이트 경계에서 시작하면 앞 절반은 이번 블록에, 뒤 절반은 다음 블록에 있을 수 있다. 그래서 C를 지원하는 코어에는 남은 절반을 잠시 들고 있다가 다음 블록과 이어 붙이는 정렬 버퍼(alignment buffer, instruction queue)가 있다.
인출 블록과 정렬 버퍼
페이지 경계에 걸치면: 반쪽짜리 페이지 폴트
더 곤란한 경우는 32비트 명령이 가상 메모리 페이지 경계에 걸칠 때다. 앞 페이지는 메모리에 있고 뒤 페이지는 아직 없으면, 명령의 뒤 절반을 가져오다가 페이지 폴트가 난다. 이때 특권 규격은 mepc(또는 sepc)에는 명령의 시작 주소를 남기고, mtval에 0이 아닌 값을 쓰는 구현이라면 mtval에는 실패한 부분의 주소를 남기도록 정한다. 운영체제는 mtval이 가리키는 페이지를 채운 뒤 mepc로 돌아가 명령 전체를 처음부터 다시 실행한다(18장, 19장).
0x0ffe에서 시작하는 32비트 jal은 페이지 A와 B에 걸쳐 있다. 앞 절반 인출은 성공하지만 뒤 절반에서 폴트가 난다. 트랩 처리기는 mtval로 어느 페이지를 채울지 알고, mepc로 명령의 처음부터 다시 실행한다. C가 없는(IALIGN=32) 시스템에서는 4바이트 정렬된 명령이 4 KiB 페이지 경계에 걸칠 일이 없다.여러 명령을 한 사이클에 해독하는 슈퍼스칼라 코어는 각 명령의 시작 위치를 먼저 찾아야 한다. RVC는 2바이트 묶음마다 하위 두 비트만 보면 길이를 알 수 있어 이 일이 x86처럼 어렵지는 않지만, 명령 경계를 찾는 회로와 정렬 버퍼는 분명히 추가 비용이다. 이 비용 때문에 응용 프로세서 쪽 일부에서는 C를 의무로 둘지 논쟁이 있었다. 반대로 인출 대역폭과 명령어 캐시 용량을 25–30% 아끼는 효과는 고성능 코어에도 그대로 이득이다. 현재 RVA 프로파일(23장)은 C를 포함한다.
핵심 정리
- C 확장은 자주 쓰는 명령에 16비트 인코딩을 더해 코드 크기를 보통 25–30% 줄인다. 명령어 캐시 효율과 인출 대역폭도 함께 좋아진다.
inst[1:0] ≠ 11이면 16비트 명령이다. 세 값이 사분면 0·1·2이고, 각 사분면은 funct3로 다시 나뉜다. C가 있으면 IALIGN=16이 되어 32비트 명령도 2바이트 경계에서 시작할 수 있다.- 모든 RVC 명령은 기본 ISA(또는 F·D)의 32비트 명령 하나로 펼쳐진다. 하드웨어는 해독 앞단의 확장기만, 컴파일러는 아무것도 바꾸지 않아도 된다.
- 아홉 형식(CR·CI·CSS·CIW·CL·CS·CA·CB·CJ)은 2-주소 형식, 3비트 레지스터(x8–x15), 짧고 배율이 붙은 즉시값을 조합한 결과다.
- Zca·Zcf·Zcd는 C를 쪼갠 이름이고, Zcb는 단순한 16비트 명령을, Zcmp는 push/pop을, Zcmt는 표 점프와
jvtCSR(0x017)을 더한다. Zcmp·Zcmt는c.fsdsp자리를 다시 쓰므로 Zcd와 함께 쓸 수 없다. - 2바이트 정렬 때문에 32비트 명령이 인출 블록이나 페이지 경계에 걸칠 수 있다. 정렬 버퍼가 이를 이어 붙이고, 페이지 폴트가 나면 mepc는 명령의 시작, mtval은 실패한 부분을 가리킨다.
확인 퀴즈
메모리에서 읽은 16비트 묶음의 하위 두 비트가 10이다. 이 명령은?
11이다. 00·01·10은 각각 RVC 사분면 0·1·2다.CL 형식 c.lw의 rd′ 칸이 010이다. 실제 목적지 레지스터는?
01을 붙이면 01010 = x10 = a0이다.다음 중 압축할 수 없는 명령은?
c.lw의 오프셋은 5비트 × 4 = 0–124다. 128은 범위를 넘는다. 나머지는 각각 c.lw, c.swsp(0–252), c.addi16sp(−512–496, 16의 배수)로 줄어든다.“모든 RVC 명령은 32비트 명령 하나로 펼쳐진다”는 제약이 주는 이득으로 맞지 않는 것은?
RV64C에서 RV32C의 c.jal 인코딩 자리는 무엇으로 쓰이나?
c.jal, RV64에서는 c.addiw다. 짧은 호출은 작은 RV32 프로그램에서 더 흔하고, RV64에서는 32비트 덧셈 후 부호 확장이 자주 필요하기 때문이다.RV64GC(= C와 D를 모두 가진) 프로세서에 Zcmp를 더할 수 없는 이유는?
c.fsdsp 자리를 Zcmp(와 Zcmt)가 재사용하므로 둘은 공존할 수 없다.16비트 값 0x0000을 실행하면?
c.nop은 0x0001이다.0x2ffe에서 시작하는 32비트 명령의 뒤 절반(0x3000)을 인출하다 페이지 폴트가 났다. mepc와 mtval은?
더 읽을거리
- The RISC-V Instruction Set Manual, Volume I — "C" Extension for Compressed Instructions, Version 2.0 (v20260120)
- Volume I — "Zc*" Extension for Code Size Reduction, Version 1.0.0 (Zca·Zcf·Zcd·Zcb·Zcmp·Zcmt, jvt CSR)
- Volume I — RV32I Base Integer Instruction Set (IALIGN과 명령 주소 정렬 예외)
- Volume II — Machine-Level ISA (mtval이 가변 길이 명령의 어느 부분을 가리키는지)
- Andrew Waterman, “Design of the RISC-V Instruction Set Architecture”, UC Berkeley 박사 논문 (2016) — RVC 설계와 코드 크기 측정
- David Patterson, Andrew Waterman, The RISC-V Reader: An Open Architecture Atlas (2017) — RV32C 장