Chapter 15

V 확장: 벡터 연산

배열 원소 수천 개에 같은 계산을 하는 일은 그래픽, 신호 처리, 머신러닝, 심지어 memcpy와 strlen에도 흔하다. 한 명령으로 여러 원소를 처리하는 방법으로 x86과 Arm은 128·256·512비트처럼 폭이 정해진 SIMD를 세대마다 새로 추가했다. RISC-V V 확장은 레지스터 길이를 구현에 맡기고, 프로그램은 "몇 개 남았는지"만 하드웨어에 알려 주는 벡터 길이 무관(vector-length agnostic) 방식을 택했다. 같은 바이너리가 128비트 칩에서도 1024비트 칩에서도 돌아간다. 이 장에서는 그 비밀인 VLEN·SEW·LMUL·vl과 vsetvli를 그림과 시뮬레이터로 해부한다.

왜 벡터인가: 고정 폭 SIMD와 길이 무관 벡터

스칼라 코드로 z[i] = x[i] + y[i]를 n번 계산하면 매 원소마다 로드 두 번, 덧셈 한 번, 저장 한 번, 포인터 증가와 분기가 따라온다. 계산 자체보다 명령을 가져오고 해독하는 비용이 더 크다. 한 명령에 여러 원소를 실으면 이 비용이 원소 수만큼 나뉜다. 여기까지는 SIMD와 벡터가 같다. 차이는 몇 개를 싣느냐를 누가 정하느냐에 있다.

고정 폭 SIMD: 폭이 바뀌면 명령도 바뀐다 SSE · 128비트 레지스터 (32비트 원소 4개) AVX · 256비트 레지스터 (원소 8개, 새 명령) AVX-512 · 512비트 레지스터 (원소 16개, 또 새 명령) 폭마다 새 명령·새 바이너리가 필요하다 n이 폭의 배수가 아니면 남은 원소용 스칼라 꼬리 루프도 따로 쓴다 RISC-V V: 길이에 얽매이지 않는 한 바이너리 vsetvli t0, a0, e32, m1 vle32.v · vadd.vv · vse32.v 같은 기계어 VLEN = 128 한 번에 4원소 VLEN = 256 한 번에 8원소 VLEN = 512 한 번에 16원소 하드웨어가 "이번에 vl개 처리한다"고 알려 준다
그림 15-1. 고정 폭 SIMD(왼쪽)는 레지스터 폭이 넓어질 때마다 새 명령 집합과 새 바이너리가 필요하다. RISC-V 벡터(오른쪽)는 같은 기계어가 VLEN이 다른 세 칩에서 각자 할 수 있는 만큼(4, 8, 16원소)씩 처리한다. 금색 점은 같은 명령어가 세 구현으로 흘러가는 모습이다.

RISC-V V 확장은 1970년대 Cray-1에서 시작한 벡터 머신의 전통을 잇는다. 레지스터 하나의 길이 VLEN은 구현이 정하는 상수이고, 프로그램은 처리할 원소 수(응용 벡터 길이(AVL, application vector length))를 vsetvli로 건네 하드웨어가 이번 반복에 처리할 개수 vl을 돌려받는다. 이렇게 짠 코드를 벡터 길이 무관(VLA, vector-length agnostic) 코드라 한다.

고정 폭 SIMD (예: SSE/AVX)RISC-V V
레지스터 폭명령 집합마다 고정 (128/256/512)구현마다 다른 VLEN (2의 거듭제곱, 최대 216비트)
원소 폭명령마다 따로 (addps, addpd …)vtype의 SEW가 정한다. 명령은 하나 (vadd.vv)
남은 원소 처리별도의 스칼라 꼬리 루프마지막 반복에서 vl이 줄어들 뿐, 같은 루프
하드웨어가 넓어지면새 명령으로 다시 컴파일같은 바이너리가 그대로 빨라진다
명령 인코딩폭·형식 정보가 opcode에 들어간다32비트 명령 안에 다 못 넣는 정보는 vtype CSR로 뺐다
오래 걸린 비준

RISC-V 벡터 확장은 여러 해에 걸쳐 여러 초안(v0.7, v0.8, v0.9 …)을 거친 끝에 2021년 v1.0으로 비준되었다. 초안 v0.7.1을 구현한 칩이 먼저 시장에 나오기도 해서, "RVV 0.7.1"과 "RVV 1.0"은 서로 호환되지 않는다는 점을 기억해 두면 좋다. 이 장은 비준된 v1.0만 다룬다.

벡터 레지스터와 CSR

V 확장은 정수·부동소수점 레지스터와 별도로 32개의 벡터 레지스터 v0–v31을 더한다. 각 레지스터는 VLEN비트다. 규격은 구현마다 두 상수를 정하게 한다.

벡터 레지스터 32개 VLEN 비트 (구현이 정하는 상수: 128, 256, 512, …) v0 마스크(v0.t)로도 쓰인다 v1 원소 폭(SEW)과 묶음(LMUL)은 vtype이 정한다 v2 v3 ⋮ v30 v31 ELEN: 원소 하나의 최대 폭 (V 확장은 64) · VLEN ≥ ELEN mstatus.VS[10:9] = Off 이면 모든 벡터 명령과 벡터 CSR 접근이 illegal-instruction 예외 벡터 CSR 7개 주소이름뜻 0x008vstart시작 원소 번호 0x009vxsat고정소수점 포화 플래그 0x00Avxrm고정소수점 반올림 모드 0x00Fvcsrvxrm + vxsat 묶음 0xC20vl벡터 길이 (원소 수) 0xC21vtype원소 폭 · 묶음 · 정책 0xC22vlenbVLEN/8 (바이트) URW 사용자 읽기·쓰기 URO 읽기 전용
그림 15-2. V 확장의 프로그래머 모델. 벡터 레지스터 32개는 모두 VLEN비트이며, 그 안을 몇 비트짜리 원소로 나눌지는 vtype이 정한다(v2는 8칸, v3는 4칸으로 나눈 예). 오른쪽은 규격 표 그대로의 CSR 주소다. vl·vtype·vlenb는 읽기 전용 CSR이다. vl과 vtype은 vset{i}vl{i} 명령으로 바뀌고(vl은 fault-only-first 로드로도 줄어든다), vlenb는 설계 시점 상수다.
CSR주소권한설명
vstart0x008URW벡터 명령이 시작할 원소 번호. 트랩이 난 원소를 기록해 재개에 쓴다. 모든 벡터 명령은 끝나면 0으로 되돌린다.
vxsat0x009URW고정소수점 연산이 포화(saturate)되었는지 표시하는 1비트 플래그.
vxrm0x00AURW고정소수점 반올림 모드 2비트(rnu, rne, rdn, rod).
vcsr0x00FURWvxrm(비트 2:1)과 vxsat(비트 0)을 한꺼번에 읽고 쓰는 창.
vl0xC20URO이번 명령이 갱신할 원소 수.
vtype0xC21URO원소 폭(SEW), 레지스터 묶음(LMUL), 꼬리·마스크 정책, vill.
vlenb0xC22UROVLEN/8. 레지스터 하나의 바이트 수. 설계 시점 상수다.
vlenb가 따로 있는 이유

문맥 교환 코드는 벡터 레지스터를 저장할 스택 공간을 계산해야 한다. vlenb가 없으면 vsetvli로 VLMAX를 알아내야 하는데, 그러면 저장하려던 vl과 vtype이 망가진다. 그래서 규격은 VLEN/8을 그냥 읽을 수 있는 CSR을 두었다. 운영체제가 쓰는 mstatus.VS 필드(Off/Initial/Clean/Dirty)는 부동소수점의 FS와 같은 방식으로, 벡터 상태가 더러워졌을 때만 저장하게 해 문맥 교환 비용을 줄인다(17장).

SEW·LMUL·vtype: 레지스터를 어떻게 나누고 묶을까

벡터 명령의 32비트 인코딩에는 원소 폭이나 레지스터 묶음 정보를 넣을 자리가 없다. 그래서 RISC-V는 이 정보를 vtype CSR로 빼냈다. vadd.vv는 하나뿐이지만, 직전에 vsetvli로 정한 vtype에 따라 8비트 덧셈이 되기도 하고 64비트 덧셈이 되기도 한다.

XLEN-1 XLEN-2 … 8 7 6 5 … 3 2 … 0 vill 0 (예약) vma vta vsew[2:0] vlmul[2:0] 잘못된 설정 0으로 둔다 마스크 정책 꼬리 정책 SEW 선택 LMUL 선택 예: vsetvli t0, a0, e32, m4, ta, ma 가 쓰는 vtype = 0xD2 0 0 … 0 1 1 010 → SEW=32 010 → LMUL=4
그림 15-3. vtype의 비트 배치. vill은 맨 위 비트(XLEN-1)에 있어서, 소프트웨어는 vtype을 읽어 "음수인가"만 분기로 확인하면 설정이 거부되었는지 알 수 있다. vill이 1이면 나머지 비트는 모두 0이고 vl도 0이 된다.
vsew[2:0]SEW어셈블러
0008e8
00116e16
01032e32
01164e64
1xx예약
vlmul[2:0]LMULVLMAX
100예약—
101mf8 1/8VLEN/SEW/8
110mf4 1/4VLEN/SEW/4
111mf2 1/2VLEN/SEW/2
000m1 1VLEN/SEW
001m2 22·VLEN/SEW
010m4 44·VLEN/SEW
011m8 88·VLEN/SEW

vlmul은 부호 있는 3비트 값으로, LMUL = 2vlmul이다. 그래서 111(−1)이 1/2, 110(−2)이 1/4, 101(−3)이 1/8이 된다. 여기서 이 장의 핵심 공식이 나온다.

VLMAX = LMUL × VLEN / SEW

LMUL: 레지스터 여러 개를 하나처럼

LMUL(length multiplier)이 1보다 크면 연속된 레지스터 여러 개가 레지스터 그룹(vector register group) 하나가 된다. 명령에는 그룹의 첫 레지스터 번호만 적고, 원소는 그 레지스터부터 차례로 채워진 뒤 다음 번호로 넘어간다. 그룹 시작 번호는 LMUL의 배수여야 한다. 아니면 그 인코딩은 예약(reserved)이다.

VLEN=128, SEW=32, LMUL=4 → v8–v11 네 개가 피연산자 하나 v8 v9 v10 v11 원소 0원소 1원소 2원소 3 원소 4원소 5원소 6원소 7 원소 8원소 9원소 10원소 11 원소 12원소 13원소 14원소 15 레지스터 그룹 VLMAX = 4×128/32 = 16 LMUL=2는 짝수 번호, LMUL=4는 4의 배수, LMUL=8은 v0·v8·v16·v24에서만 그룹이 시작한다. LMUL=1/2이면 레지스터 하나의 앞쪽 절반만 쓰고, 나머지 절반은 꼬리(tail)로 취급한다.
그림 15-4. LMUL=4 레지스터 그룹. 원소는 v8을 다 채운 뒤 v9로 넘어간다(금색 점의 순서). 규격의 그림은 최하위 바이트(0번 원소)를 오른쪽에 그리지만, 이 장의 그림과 시뮬레이터는 읽기 편하게 0번을 왼쪽에 둔다.

LMUL을 키우는 첫째 이유는 폭이 다른 데이터를 같은 원소 수로 맞추기 위해서다. 16비트 값을 32비트로 넓히면 같은 원소 수를 담는 데 레지스터가 두 배 필요하다. 그래서 규격은 루프 안에서 SEW/LMUL 비율을 일정하게 유지하라고 권한다. 비율이 같으면 VLMAX도 같아서, 한 번 정한 vl을 모든 폭에 그대로 쓸 수 있다. 분수 LMUL은 반대로 좁은 데이터가 레지스터 하나를 통째로 차지하지 않게 해, 32개 레지스터 이름을 더 많이 쓰게 해 준다. ELEN=64인 구현은 1/2, 1/4, 1/8을 반드시 지원해야 한다.

SEW/LMUL →1248163264
SEW=884211/21/41/8
SEW=1684211/21/4
SEW=3284211/2
SEW=648421

위 표의 한 열에 있는 설정들은 VLMAX가 모두 같다(규격 표를 그대로 옮겼다). 예를 들어 SEW/LMUL=16 열에서는 e8·mf2, e16·m1, e32·m2, e64·m4가 같은 원소 수를 다룬다.

EXPLORER

VLEN · SEW · LMUL 탐색기

VLEN (구현)
SEW
LMUL
레지스터당 원소 (VLEN/SEW)4
VLMAX16
vl (AVL 반영)16
그룹v8–v11
vlenb16
캔버스의 레지스터 줄을 누르면 그 레지스터가 그룹의 시작이 된다. LMUL=4에서 v9를 눌러 보자(예약된 인코딩 경고). VLEN만 128 → 1024로 바꾸면 같은 vsetvli가 돌려주는 VLMAX가 8배가 된다. e64·mf8처럼 SEW가 LMUL×ELEN보다 큰 조합도 골라 보자.

vsetvli: 하드웨어와 길이를 협상하기

설정 명령은 세 가지다. 셋 다 새 vtype을 쓰고, AVL로부터 vl을 정해 vl CSR과 rd에 함께 쓴다. 모두 OP-V 주 opcode(1010111)의 funct3=111(OPCFG)을 쓴다.

vsetvli rd, rs1, vtypei # rd = 새 vl, rs1 = AVL, vtypei = 새 vtype (즉시값) vsetivli rd, uimm, vtypei # rd = 새 vl, uimm = AVL (5비트, 0–31) vsetvl rd, rs1, rs2 # rd = 새 vl, rs1 = AVL, rs2 = 새 vtype 값 (문맥 복원용)
vsetvli rd, rs1, vtypei 0 zimm[10:0] = vtypei rs1 (AVL) 111 rd 1010111 (OP-V) 3130201915141211760 vsetivli rd, uimm, vtypei 11 zimm[9:0] = vtypei uimm[4:0] 111 rd 1010111 (OP-V) 313029201915 vsetvl rd, rs1, rs2 1 000000 rs2 (vtype) rs1 (AVL) 111 rd 1010111 (OP-V) 3130252420
그림 15-5. 설정 명령 세 가지의 인코딩. 비트 31(과 30)이 셋을 구별한다. vsetvli는 vtype의 아래 11비트를, vsetivli는 아래 10비트를 즉시값으로 담는다. 명령 안에서 vlmul은 비트 22:20, vsew는 25:23, vta는 26, vma는 27에 놓인다.

AVL은 어디서 오나

rdrs1AVL효과
아무거나x0 아님x[rs1]의 값보통의 스트립 마이닝
x0 아님x0~0 (가장 큰 부호 없는 수)vl = VLMAX로 설정하고 rd에도 쓴다
x0x0현재 vl 값vl은 그대로 두고 vtype만 바꾼다. 새 SEW/LMUL 비율 때문에 VLMAX가 바뀌면 예약

vl을 정하는 규칙

AVL이 VLMAX 이하면 vl = AVL이다. AVL이 VLMAX의 두 배 이상이면 vl = VLMAX다. 그 사이, 즉 VLMAX < AVL < 2·VLMAX인 경우에만 구현에 재량이 있다. vl은 ceil(AVL/2) 이상 VLMAX 이하 중 아무 값이나 될 수 있다. 같은 구현에서 같은 입력이면 늘 같은 값이어야 한다.

규격 원문에서
"vl = AVL if AVL ≤ VLMAX; ceil(AVL / 2) ≤ vl ≤ VLMAX if AVL < (2 * VLMAX); vl = VLMAX if AVL ≥ (2 * VLMAX)" … "this permits an implementation to set vl = ceil(AVL / 2) for VLMAX < AVL < 2*VLMAX in order to evenly distribute work over the last two iterations of a strip-mine loop."

Volume I, "V" Extension — Constraints on Setting vl — 마지막 두 반복에 일을 고르게 나누는 구현을 허용하려는 규칙이다. 소프트웨어는 vl이 VLMAX라고 가정하지 말고 반드시 돌려받은 값을 써야 한다.

이 규칙에서 유용한 성질이 따라 나온다. AVL=0이면 vl=0, AVL>0이면 vl>0, 그리고 vl은 VLMAX와 AVL을 넘지 않는다. 또 첫 반복의 vl이 모든 반복 중 가장 크다는 것이 보장되므로, 리덕션 루프가 vl의 최댓값을 따로 기억할 필요가 없다.

SIMULATOR

vsetvli 인코딩 만들기

vsetvli t0, a0, e32, m4, ta, ma

명령
SEW
LMUL
꼬리
마스크
기계어0x0d2572d7
vtype 값0xd2
AVLx[a0]
VLMAX (VLEN=128)16
기본값 vsetvli t0, a0, e32, m1, ta, ma(LMUL을 m1로)가 0x0d0572d7이 되는지 확인해 보자. rd와 rs1을 모두 zero로 두면 어떤 뜻이 되는지, vsetivli에서 uimm이 비트 19:15에 들어가는 모습도 살펴보자.

스트립 마이닝: 길이 무관 루프

n개짜리 배열을 VLMAX개씩 잘라 처리하는 기법을 스트립 마이닝(strip mining)이라 한다. RISC-V에서는 vsetvli가 자르는 일을 맡는다. 아래는 규격 부록의 32비트 정수 벡터 덧셈 예제다. 반복 횟수도, 마지막 꼬리 처리도 코드에 드러나지 않는다. t0에 돌아온 vl만큼 포인터를 옮기고 남은 수 a0를 줄일 뿐이다.

# a0 = n, a1 = x, a2 = y, a3 = z vvaddint32: vsetvli t0, a0, e32, m1, ta, ma # 32비트 원소로 vl 정하기 vle32.v v0, (a1) # x 벡터 로드 sub a0, a0, t0 # 남은 수 -= vl slli t0, t0, 2 # vl × 4바이트 add a1, a1, t0 # x 포인터 이동 vle32.v v1, (a2) # y 벡터 로드 add a2, a2, t0 vadd.vv v2, v0, v1 # 원소별 덧셈 vse32.v v2, (a3) # z 저장 add a3, a3, t0 bnez a0, vvaddint32 # 남았으면 반복 ret
AVL = 37, VLMAX = 16 (VLEN=128 · e32 · m4) 구현 ① 1회 vl=16 2회 vl=16 3회 vl=5 남은 AVL=21 은 2×16보다 작지만 VLMAX를 고른다 → 마지막 반복이 5개뿐 구현 ② 1회 vl=16 2회 vl=11 3회 vl=10 AVL=21 < 2×16 이므로 ceil(21/2)=11 도 허용 → 마지막 두 반복이 고르게 나뉜다
그림 15-6. 같은 바이너리, 같은 VLEN이라도 구현에 따라 vl 분배가 다를 수 있다. 두 구현 모두 규격을 지킨다. 코드는 t0에 돌아온 값만 쓰므로 어느 쪽이든 결과가 같다.
ANIMATION

스트립 마이닝 루프 돌려 보기

VLEN (하드웨어)
LMUL (e32)
vl 정책 (VLMAX < AVL < 2·VLMAX)
실행
VLMAX16
이번 vl (t0)—
남은 원소 (a0)37
반복 / 전체0 / 3
n을 그대로 두고 VLEN만 128 → 1024로 바꿔 보자. 코드는 한 글자도 바뀌지 않았는데 반복 횟수가 줄어든다. vl 정책을 ceil(AVL/2)로 바꾸면 n=37, VLMAX=16에서 마지막 두 반복이 11개와 10개로 나뉜다.

마스크와 꼬리 정책

if가 들어 있는 루프를 벡터로 바꾸려면 원소마다 연산을 할지 말지 고를 수 있어야 한다. V 확장은 이를 마스크(mask)로 한다. 마스크는 항상 v0에서 오고, 원소 i의 마스크 비트는 SEW·LMUL과 상관없이 v0의 비트 i다. 명령의 비트 25인 vm이 0이면 마스크를 쓰고(어셈블리에서 v0.t), 1이면 쓰지 않는다.

vmslt.vi v0, v8, 5 # v0[i] = (v8[i] < 5) vadd.vv v4, v8, v12, v0.t # vm=0: v0[i]=1 인 원소만 더한다 vadd.vv v4, v8, v12 # vm=1: 모든 원소

한 명령이 다루는 원소 번호는 네 부류로 나뉜다. vstart보다 앞은 prestart, vstart부터 vl 앞까지는 body, vl부터 끝까지는 tail이다. body는 다시 마스크가 켜진 active와 꺼진 inactive로 나뉜다.

vstart=3 vl=11 VLMAX=16 원소 v0.mask 분류 0123456789101112131415 1 0 1 1 0 1 1 0 1 0 1 1 0 1 0 1 유지 유지 유지 새값 비활성 새값 새값 비활성 새값 비활성 새값 꼬리 꼬리 꼬리 꼬리 꼬리 prestart 갱신하지 않음 body = active + inactive inactive 는 vma 정책을 따른다 tail vta 정책을 따른다 active 원소만 예외를 일으킬 수 있고, 새 결과를 쓴다
그림 15-7. 원소 번호의 네 부류. vstart=3, vl=11, VLMAX=16인 마스크 명령의 예다. vstart는 보통 0이고, 트랩 뒤 재개할 때만 0이 아니다.

그렇다면 inactive와 tail 자리의 목적지 레지스터에는 무엇이 남을까? vtype의 두 비트가 정한다.

vtavma어셈블러tail 원소inactive 원소
00tu, muundisturbed (이전 값 유지)undisturbed
01tu, maundisturbedagnostic
10ta, muagnosticundisturbed
11ta, maagnosticagnostic
규격 원문에서
"When a set is marked agnostic, the corresponding set of destination elements in any vector destination operand can either retain the value they previously held, or are overwritten with 1s."

Volume I, "V" Extension — vta and vma — agnostic은 "아무 값"이 아니라 "이전 값 또는 모두 1"이다. 원소마다 둘 중 어느 쪽이 될지는 정해지지 않으며, 같은 입력으로 다시 실행해도 달라질 수 있다.

왜 agnostic이 필요한가

레지스터 이름 바꾸기(renaming)를 하는 비순차 코어에서 결과는 새 물리 레지스터에 쓰인다. undisturbed라면 쓰지 않는 원소까지 옛 물리 레지스터에서 읽어 복사해야 한다. agnostic이면 그 복사를 생략할 수 있다. 0이 아니라 "모두 1"로 정한 것은 소프트웨어가 그 값에 기대지 않게 하려는 의도다. 단순한 순차 코어는 설정을 무시하고 늘 undisturbed로 동작해도 된다. 마스크를 만드는 명령의 tail은 vta와 상관없이 항상 agnostic이다.

SIMULATOR

마스크와 꼬리 정책 시각화

원소 번호 (VLEN=128, e16, m1 → VLMAX=8)
v0.mask — 눌러서 바꾸기
새 결과 v8[i] + v12[i]
실행 전 v4[i]
실행 후 v4[i]
active: 새 값inactivetailprestartagnostic: 이전 값 또는 FFFF
마스크 사용
꼬리
비활성
agnostic
active0
inactive0
tail0
ta·ma로 바꾸고 "다시 실행"을 여러 번 눌러 보자. 점선 칸은 실행할 때마다 이전 값이 남기도 하고 FFFF(SEW=16의 모두 1)가 되기도 한다. vstart를 vl 이상으로 올리면 아무 원소도 바뀌지 않는다는 규칙도 확인하자.

벡터 메모리 접근

벡터 로드·저장은 새 opcode를 만들지 않고 스칼라 부동소수점 로드·저장 opcode(LOAD-FP, STORE-FP) 안의 남는 width 값을 썼다. 메모리 원소 폭 EEW는 명령에 직접 적혀 있어서(vle8, vle16, vle32, vle64) 폭이 섞인 루프에서도 vtype을 덜 바꿔도 된다. 주소 지정 방식은 mop[1:0] 필드가 정한다.

vle32.v v8, (a0) 단위 보폭 (unit-stride) 연속된 원소 +0+4+8+12 메모리 (4바이트 칸) v8[0]v8[1]v8[2]v8[3] vlse32.v v8, (a0), t1 상수 보폭 (strided) t1 = 12바이트 간격 +0+12+24+36 v8[0]v8[1]v8[2]v8[3] vluxei32.v v8, (a0), v4 인덱스 (indexed) v4 = {20, 4, 44, 32} +4+20+32+44 v8[0]v8[1]v8[2]v8[3]
그림 15-8. 세 가지 주소 지정 방식. 상수 보폭은 x 레지스터의 바이트 간격(음수·0도 가능)을, 인덱스 방식은 벡터 레지스터에 담긴 바이트 오프셋을 기준 주소에 더한다. 인덱스 방식은 흩어 모으기(gather/scatter)라고도 부른다.
mop[1:0]로드저장설명
00vle<EEW>.vvse<EEW>.v단위 보폭. lumop로 마스크 로드(vlm.v), 통째 레지스터 로드, fault-only-first를 구별
01vluxei<EEW>.vvsuxei<EEW>.v인덱스, 순서 보장 없음 (EEW는 인덱스 폭, 데이터는 SEW)
10vlse<EEW>.vvsse<EEW>.v상수 보폭
11vloxei<EEW>.vvsoxei<EEW>.v인덱스, 원소 순서대로 접근 (입출력 장치 영역용)

세그먼트 로드: 구조체 배열을 필드별로

RGB 픽셀처럼 필드 여러 개가 반복되는 "구조체의 배열"은 세그먼트(segment) 로드로 한 번에 풀 수 있다. nf 필드가 세그먼트당 필드 수(2–8)를 정하고, 각 필드가 연속된 번호의 레지스터(그룹)로 들어간다. EMUL × 필드 수는 8을 넘을 수 없다.

vlseg3e8.v v8, (a0) — 3바이트 세그먼트(R, G, B)를 레지스터 세 개로 메모리 R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 R0R1R2R3 G0G1G2G3 B0B1B2B3 v8 ← 빨강 v9 ← 초록 v10 ← 파랑
그림 15-9. 세그먼트 로드는 메모리의 R·G·B 바이트를 필드별 레지스터로 갈라 넣는다(규격 예제 1). vl은 세그먼트 수이고, 마스크도 세그먼트 단위로 적용된다. 저장 vsseg3e8.v는 반대 방향이다.

fault-only-first: 끝을 모르는 루프

strlen처럼 어디서 끝날지 모르는 루프를 벡터로 돌리면, 문자열 끝 뒤의 바이트를 미리 읽다가 매핑되지 않은 페이지를 건드릴 수 있다. vle8ff.v 같은 fault-only-first 로드는 0번 원소의 예외만 트랩으로 처리한다. 1번 이후 원소에서 예외가 날 자리라면 트랩 없이 vl을 그 원소 번호로 줄이고 끝낸다. 아래는 규격의 예제다.

strlen: mv a3, a0 # 시작 주소 저장 loop: vsetvli a1, x0, e8, m8, ta, ma # 바이트 벡터, 최대 길이(AVL=~0) vle8ff.v v8, (a3) # 바이트 로드 (뒤쪽 폴트는 vl만 줄인다) csrr a1, vl # 실제로 읽은 바이트 수 vmseq.vi v0, v8, 0 # v8[i] == 0 이면 v0[i] = 1 vfirst.m a2, v0 # 첫 번째 1의 위치, 없으면 -1 add a3, a3, a1 # 포인터 이동 bltz a2, loop # 0바이트를 못 찾았으면 반복 add a0, a0, a1 add a3, a3, a2 sub a0, a3, a0 # 길이 = 끝 - 시작 ret
fault-only-first의 보안 고민

트랩 없이 "어디까지 읽을 수 있는지"를 알려 주므로, 주소 공간을 몰래 탐색하는 데 쓰일 수 있다. 그래서 규격은 이 형태를 단위 보폭 로드에만 두었다. 이미 아는 영역 바로 뒤만 탐색할 수 있게 해 위험을 줄인 것이다. 14장의 orc.b를 쓴 스칼라 strlen(14장)과 비교해 보면 같은 문제를 푸는 두 가지 방법이 보인다.

넓히기·리덕션·순열

산술 명령은 OP-V opcode에 있고, funct3가 피연산자 종류를 정한다. .vv(벡터-벡터), .vx(벡터-정수 레지스터), .vi(벡터-5비트 즉시값), 부동소수점의 .vf다. 대부분은 이름만 보면 뜻을 알 수 있으니, 여기서는 벡터만의 독특한 무리 셋을 본다.

넓히기와 좁히기

vw로 시작하는 넓히기(widening) 명령은 SEW 원소 둘로 2·SEW 결과를 만든다. 원소 수는 그대로이므로 결과는 레지스터 그룹이 두 배로 커진다. 규격의 표현으로 목적지의 EEW = 2·SEW, EMUL = 2·LMUL이다. 그래서 LMUL=8에서의 넓히기는 EMUL=16이 되어 예약이다. 반대로 vn으로 시작하는 좁히기(narrowing)는 2·SEW 원본을 SEW로 줄인다.

vwadd.vv v8, v4, v6 (SEW=16, LMUL=1, VLEN=128) — 결과는 두 배 폭, 두 배 레지스터 v4 v6 v8 v9 a0a1a2a3a4a5a6a7 b0b1b2b3b4b5b6b7 a0+b0a1+b1a2+b2a3+b3 a4+b4a5+b5a6+b6a7+b7 EEW=16EMUL=1 EEW=32EMUL=2 원소 8개는 그대로인데 폭이 두 배라서 v8·v9 두 레지스터를 차지한다. 그래서 목적지는 짝수 번호여야 한다.
그림 15-10. 넓히기 덧셈 vwadd.vv. 원소 i의 결과는 원소 i 자리에 그대로 놓이지만, 폭이 두 배여서 앞의 넷은 v8, 뒤의 넷은 v9에 들어간다. 금색 점은 a2·b2, a5·b5, a7·b7의 결과가 가는 길이다.

리덕션: 벡터를 스칼라 하나로

vredsum.vs vd, vs2, vs1은 vs2의 active 원소를 모두 더하고 vs1[0]을 보탠 값을 vd[0]에 쓴다. 스칼라 입력과 출력이 x 레지스터가 아니라 벡터 레지스터의 0번 원소에 있는 것이 특징이다. 벡터 유닛과 스칼라 유닛을 느슨하게 분리해 두려는 설계다.

vredsum.vs v1, v8, v2 — v1[0] = v2[0] + Σ v8[i] (vl=8) 3 1 4 1 5 9 2 6 v8 4 5 14 8 9 22 31 + v2[0] = 10 = v1[0] = 41 정수 합은 순서와 무관해 트리로 더해도 된다. 부동소수점은 vfredosum(순서 보장)과 vfredusum이 따로 있다.
그림 15-11. 합 리덕션. inactive 원소는 빠지지만 v2[0]은 마스크와 상관없이 늘 포함된다. vl=0이면 아무것도 쓰지 않는다. v1의 나머지 원소(1 이상)는 꼬리로 취급된다.

순열: 원소의 자리를 옮기기

원소의 위치를 바꾸는 명령은 vslideup/vslidedown(정해진 칸 수만큼 밀기), vslide1up/vslide1down(한 칸 밀며 스칼라 끼워 넣기), vrgather(인덱스 벡터로 아무 원소나 골라 모으기. 인덱스가 VLMAX 이상이면 0), vcompress(마스크로 고른 원소를 앞으로 모으기)가 있다.

vslideup.vi v2, v1, 2 (vl=8) v1 v2 a b c d e f g h 이전 이전 a b c d e f 0–1번은 바뀌지 않는다 (OFFSET 앞) g, h는 vl 범위 밖으로 밀려나 버려진다 목적지와 원본은 겹칠 수 없다 vcompress.vm v2, v1, v0 (vl=9, tu) v0 v1 v2 후 v2 전 101001011 0 1 2 3 4 5 6 7 8 0 2 5 7 8 4 3 2 1 9 8 7 6 5 4 3 2 1 앞으로 모으고, 남은 자리(금색)는 tu 정책으로 이전 값
그림 15-12. 왼쪽: vslideup은 원소를 OFFSET만큼 뒤로 민다. 오른쪽: 규격의 vcompress 예제를 0번 원소가 왼쪽에 오도록 다시 그렸다. 마스크가 1인 원소 0·2·5·7·8이 결과의 앞쪽 다섯 칸에 모이고, 남은 네 칸은 tu 정책이라 실행 전 v2 값이 남는다.
마스크를 다루는 명령도 있다

마스크끼리의 논리 연산(vmand.mm, vmnot.m 등), 마스크의 1 개수(vcpop.m), 첫 1의 위치(vfirst.m), 첫 1 앞까지 켜기(vmsbf.m), 누적 번호(viota.m) 같은 명령이 조건문과 while 루프의 벡터화를 돕는다. 앞의 strlen 예제의 vfirst.m이 그 예다.

Zve*·Zvl*와 벡터 암호 확장

V 확장 전체는 작은 마이크로컨트롤러에게 무겁다. 64비트 원소, 배정밀도 부동소수점, VLEN 128비트를 모두 갖추어야 하기 때문이다. 그래서 규격은 임베디드용 Zve* 다섯 가지와 최소 VLEN을 밝히는 Zvl*를 따로 정의한다.

화살표 방향으로 갈수록 앞의 확장을 포함하며 기능이 늘어난다 Zve32xVLEN≥32 · EEW 8–32 Zve32f+ FP32 (F 필요) Zve64xVLEN≥64 · EEW 8–64 Zve64f+ FP32 Zve64d+ FP64 (D 필요) Zvl128bVLEN ≥ 128 V응용 프로세서
그림 15-13. 벡터 확장의 가계도. Zve32f와 Zve64x는 Zve32x에, Zve64f는 그 둘에, Zve64d는 Zve64f에 의존한다. 응용 프로세서용 V는 Zve64d와 Zvl128b에 의존한다. 모든 Zve*와 V는 정밀한(precise) 트랩을 제공한다.
확장최소 VLEN지원 EEWFP32FP64
Zve32x328, 16, 32NN
Zve32f328, 16, 32YN
Zve64x648, 16, 32, 64NN
Zve64f648, 16, 32, 64YN
Zve64d648, 16, 32, 64YY
V1288, 16, 32, 64YY

Zve64*에는 EEW=64의 vmulh 계열과 vsmul이 빠져 있다. 64비트 곱의 상위 절반을 만드는 데 게이트가 많이 들기 때문이다. Zvl 확장은 Zvl32b, Zvl64b, Zvl128b, Zvl256b, Zvl512b, Zvl1024b처럼 최소 VLEN을 밝히며, 긴 것은 짧은 것을 모두 포함한다. 프로파일은 이것으로 "이 플랫폼의 VLEN은 적어도 얼마"를 요구할 수 있다(23장).

ISA 문자열 예

RV64GCV와 RV64GCV_Zve64f는 같은 뜻이다. V가 이미 Zve64f를 포함하기 때문이다. 규격은 이렇게 겹치는 확장을 함께 적는 것을 허용한다. 반대로 RV32IMC_Zve32x라면 정수 벡터만 있는 작은 MCU다.

벡터 암호 확장

14장의 스칼라 암호(Zk*)처럼, 벡터에도 암호 확장(Vector Cryptography v1.0)이 있다. AES 한 블록은 128비트이므로 이 명령들은 SEW=32 원소 네 개를 묶은 원소 그룹(element group, EGW=128) 단위로 동작한다. VLEN이 128보다 작으면 LMUL로 레지스터를 묶어 그룹을 만든다.

확장내용
Zvbb · Zvkb벡터 기본 비트 조작(vandn, vbrev8, vrev8, 회전 등). Zvkb는 Zvbb의 암호용 부분집합
Zvbc벡터 캐리 없는 곱셈 vclmul/vclmulh (SEW=64 전용, Zve64x 필요)
ZvkgGCM/GMAC의 GHASH
ZvknedAES 암·복호화와 키 스케줄 (vaesef, vaesem …)
Zvknha · ZvknhbSHA-256 / SHA-256 + SHA-512 (Zvknhb는 Zve64x 필요)
Zvksed · Zvksh중국 표준 SM4 블록 암호 / SM3 해시
Zvkt나열된 벡터 명령의 데이터 무관 실행 시간 보장
Zvkn · Zvks묶음 이름. Zvkn = Zvkned + Zvknhb + Zvkb + Zvkt, Zvks = Zvksed + Zvksh + Zvkb + Zvkt (뒤에 c·g가 붙으면 Zvbc·Zvkg 추가)

핵심 정리

  1. V 확장은 벡터 길이 무관(VLA) 방식이다. 프로그램은 AVL을 건네고 하드웨어가 돌려준 vl만큼 처리하므로, 같은 바이너리가 VLEN이 다른 칩에서 모두 돈다.
  2. 레지스터는 v0–v31(각 VLEN비트), CSR은 vstart(0x008), vxsat(0x009), vxrm(0x00A), vcsr(0x00F), vl(0xC20), vtype(0xC21), vlenb(0xC22)다.
  3. vtype은 vill(XLEN-1), vma(7), vta(6), vsew(5:3), vlmul(2:0)로 이루어지며, VLMAX = LMUL × VLEN / SEW이다. LMUL>1이면 시작 번호가 LMUL의 배수인 레지스터 그룹을 쓴다.
  4. vsetvli는 AVL ≤ VLMAX면 vl=AVL, AVL ≥ 2·VLMAX면 vl=VLMAX, 그 사이면 ceil(AVL/2)~VLMAX 중 하나를 고른다. 코드는 늘 돌려받은 vl을 써야 한다.
  5. 마스크는 v0에서 오고(vm=0), 원소는 prestart·active·inactive·tail로 나뉜다. agnostic 정책은 "이전 값 또는 모두 1"이다.
  6. 메모리 접근은 단위 보폭·상수 보폭·인덱스(순서 보장/비보장)·세그먼트가 있고, fault-only-first 로드로 strlen 같은 while 루프를 벡터화한다.
  7. 넓히기 결과는 EEW=2·SEW, EMUL=2·LMUL이다. 리덕션은 벡터 레지스터 0번 원소를 스칼라로 쓴다. V는 Zve64d + Zvl128b에 의존하고, 임베디드용 Zve32x~Zve64d가 따로 있다.

확인 퀴즈

VLEN=256, SEW=16, LMUL=2일 때 VLMAX는?

VLMAX = LMUL × VLEN / SEW = 2 × 256 / 16 = 32이다.

LMUL=4일 때 레지스터 그룹의 시작으로 쓸 수 있는 것은?

LMUL=4 그룹은 4의 배수 번호에서만 시작한다. 다른 번호는 예약된 인코딩이다.

VLMAX=16인 설정에서 AVL=20으로 vsetvli를 실행했다. 규격이 허용하지 않는 vl은?

AVL(20) < 2·VLMAX(32)이므로 ceil(20/2)=10 ≤ vl ≤ 16이어야 한다. 8은 범위 밖이다.

vta=1(tail agnostic)일 때 tail 원소의 목적지 값은?

agnostic은 "이전 값 또는 모두 1" 둘 중 하나다. 어느 쪽인지는 원소마다, 실행마다 달라도 된다.

vle8ff.v를 실행하는데 5번 원소의 주소에서 페이지 폴트가 날 상황이다. 무슨 일이 일어나나?

fault-only-first 로드는 0번 원소의 예외만 트랩으로 처리한다. 그 뒤 원소라면 vl을 그 원소 번호로 줄인다.

LMUL=8에서 넓히기 명령 vwadd.vv를 쓸 수 없는 이유는?

넓히기 결과는 EMUL=2·LMUL이다. 레지스터 그룹은 8개를 넘을 수 없으므로 예약된 인코딩이 된다.

vill 비트가 vtype의 맨 위 비트(XLEN-1)에 있는 이유로 가장 알맞은 것은?

규격 비고에 그대로 나온다. bltz 같은 부호 분기 하나로 확인할 수 있다.

응용 프로세서용 V 확장이 의존하는 확장 조합은?

V는 Zvl128b와 Zve64d에 의존한다. 그래서 VLEN ≥ 128이고 64비트 원소와 FP64를 모두 지원한다.

더 읽을거리