Branch Log · Open in interactive viewer →

2 Instructions

How exactly does the RISC-V immediate encoding notation work?

이번 장에서는 instruction을 어떻게 represent하는지를 학습한다. 들어가기 앞서 몇 가지 RISC-V("리스크 파이브"로 발음)의 특징을 살펴보자.

프로그래밍 단계에서 변수는 무한히 선언할 수 있는 것에 비해, register가 32개밖에 존재하지 않으므로 이 제약에 맞게 register가 사용되도록 처리하는 것이 중요하다. 그렇다면 왜 이러한 제약을 두었는지 의문이 생긴다. 이유는 다음과 같다. (설계 원칙 2: 작은 것이 더 빠르다.)

현재 문서에서는 64bit register width를 갖는 RV64I를 살펴본다. 기본적으로 RV32I와 공통으로 instruction(35개)을 공유하지만 몇 가지 RV64I만의 추가 instruction(12개)을 갖고 있다.

16bit, 32bit, 48bit, 64bit 등 다양한 크기의 instruction을 모두 처리할 수 있지만, 기본적으로 32bit instruction을 사용한다. 차이점은 sign extension 유무로 bit수가 register width에 알맞게 확장된다는 점이다.

본래 RV32I에서 32bit register width에 맞게 word를 load하는 instruction인 lw는, RV64I에서 여전히 32 bit word를 load하지만 register를 채우기 위해 sign extend(부호 확장)이 이루어진다.


2.1 CISC vs RISC

RISC(Reduced Instruction Set Computer)를 CISC(Complex Instruction Set Computer)와 비교하면 다음과 같은 특성을 갖는다.

또한 RISC-V를 사용하면서 얻는 비용적 이점도 있다.

license


2.2 Variables

RISC-V ISA를 살펴보기 위한 기본 예시로, 간단히 C declaration(선언) code를 보자. 프로그래머가 다음과 같이 변수를 선언하면 compiler는 이를 RISC-V instruction으로 변환한다.

// declaration을 위해서는 type, name이 필요하다.
// type는 size/interpretation, name은 address를 결정한다.
int a;      // type: integer
            // name: a

name이 갖는 정보는 다음과 같다.

type이 갖는 정보는 다음과 같다.

참고로 variable에 간단한 정수를 담았다면 다음과 같이 초기화가 된다.

# register x5가 해당 변수를 저장한다고 가정
# 0으로 초기화
add x5 x0 x0

# 7로 초기화
addi x5 x0 7

2.3 RISC-V base ISA registers

앞서 언급한 것처럼 RISC-V(RV64I)는 총 32개의 64bit register file을 갖고 있다. 이제 32개의 general purpose register가 각각 어떤 역할을 하는지 살펴보자. 참고로 관례상 RISC-V에서는 register를 x{숫자}(x0, x1, ... x31) 형태로 표현한다.

The RISC-V Architecture

RISC-V registers

위 목적에 따라 64bit general purpose registers를 나누면 다음과 같다.

32개의 general purpose register는 register file이라는 구조 속에 들어간다.


2.4 Register Operands

다음과 같은 C code가 있다고 하자. compiler는 이 C code의 variables를 register에 알맞게 할당한다.

f = (g + h) - (i + j);

C compiler가 variable을 다음과 같이 할당했다고 하자.

위 할당에 따라 C code를 RISC-V instruction(오직 arithmetic instruction)으로 바꾸면 다음과 같다.

add x5 x20 x21    # g + h
add x6 x22 x23    # i + j
sub x19 x5 x6     # f = (g + h) - (i + j)

그런데 위 예제처럼 RISC-V instruction을 수행하기 위해서는, memory에서 variables의 값을 읽어서 temporaries에 담는 과정이 필요할 것이다. 이 과정은 load instruction가 수행한다.


2.5 Data Alignment

RISC에서는 덩어리로 제일 많이 처리하는 32bit와 64bit 묶음을 각각 word(워드), doubleword(더블 워드)라고 지칭한다.

load instruction을 살펴보기 전에, RISC-V에서 data를 memory에 어떻게 align하고 읽는지 살펴보자. 우선 alignment restriction(정렬제약)을 갖는 MIPS architecture를 살펴보고 RISC-V와 비교해 보자.

MIPS memory alignment

여기서 또 중요하게 봐야할 점은 memory는 bit 단위가 아닌 byte addressing을 사용하는 부분이다. memory에는 오직 byte 단위로만 접근할 수 있으며, 이 때문에 doubleword는 8의 배수 address를 갖게 된다.

참고로 Endian의 뜻은 다음과 같다. 아래과 같은 bit가 있다고 하면 endianness(엔디안)에 따라 register에 저장된 bit들이 memory에 저장되는 순서가 달라진다.

Endian

#  MSB    LSB

#  Little-Endian
   0x44332211

#  Big-Endian
   0x11223344

2.7 Load instructions

다음은 load instruction의 예시다.

ld x9, 8(x22)

여기서 더 주목할 점은 doubleword를 load하는 ld(load doubleword)이다. ld x9, 8(x22)는 start address(x22)에서 offset 8을 더한 location에서, 8bytes를 읽어와서 x9에 저장하게 된다.

이와 비슷한 load instruction이 더 존재한다.

   📝 예제 1: long 타입 변수 불러오기   

다음 C code를 RISC-V code로 compile하라.

C 언어에서 long은 운영체제와 플랫폼에 따라 bit수 정의가 다르니 주의. 지금은 8bytes(64bit)로 가정.

long A[20];

g = h + A[8];

따라서 만약 &A[0]=2000이라면 2004, 2008, 2012 등이 A[i]의 address가 된다.

   🔍 풀이   

RISC-V code로 compile하면 다음과 같은 instruction이 된다.

ld x5 64(x22)       # base address + offset 64에 있는 data(A[8])를 x5에 load 
add x23, x21, x5    # h + A[8]를 x23에 저장

위 예제에서 A 배열은 8byte인 long type이기 때문에, doubleword를 load하는 ld를 사용했다. A[8]이므로 offset은 8*8.

만약 4byte에 해당되는 int type 배열이었다면 lw를 사용했을 것이다. A[8]이므로 offset은 8*4.

만약 이보다도 더 짧은 2byte의 short type로 선언했다면, short A[8]을 load하려면 다음과 같은 instruction이 사용되었을 것이다. offset은 8*2

lh x5 16(x22)

   📝 예제 2: long 타입 변수 연산 후 저장하기   

다음 C code를 RISC-V code로 compile하라.(long = 8bytes)

long A[100];

A[12] = h + A[8];

   🔍 풀이   

RISC-V code로 compile한 결과는 다음과 같다.

ld    x5, 64(x22)    # reg x5 gets A[8]
add   x5, x21, x5    # reg x5 gets h+A[8]
sd    x5, 96(x22)    # Stores h+A[8] to A[12]

2.7.1 Register Spilling

현재 살펴보고 있는 RISC-V architecture는 32개 register밖에 가지고 있지 않다. 그렇다면 instruction을 load할 때, register의 빈 자리가 없는 경우도 생길 것이다.(단순히 overwrite하면 큰 문제가 생길 것이다.)

이러한 문제를 방지하기 위해 register spilling 기법을 compiler가 수행해 준다.


2.7 bit extension

register는 값을 load할 때 register width인 8byte(64bit)만큼을 채워야 한다.

참고로 연산을 수행하는 ALU도 언제나 8byte씩 입력을 받는다.

그런데 ld일 때는 문제가 없겠지만, lw, lh, lb라면 남은 자리는 어떻게 처리할까? 이 경우 unsigned, signed value에 따라 값을 채워 넣게 된다.

프로그래밍 시 자료형을 보고 signed/unsigned를 판단하는 습관을 갖는 편이 좋다. 예를 들어 int 8이라면 signed type이다.


2.8 Immediate Operands

RISC-V Assembly Overview

load instruction을 사용하지 않고 operand 자리에 constant를 넣는 instruction이 존재한다.

addi x22, x22, 4    # x22 = x22 + 4

특히 for loop에서 자주 볼 수 있다.

그렇다면 어째서 immediate operand를 사용하는 걸까?

하지만 immediate operand를 사용하는 것으로 얻는 단점도 있다. 바로 immediate operand를 사용하기 위해서는 multiplexer를 추가해야 한다는 점이다.

RISC-V immediate

ALU의 op2로 immediate operand를 넣을지, register의 값을 넣을지를 결정할 수 있는 mux를 추가로 달아줘야 한다.

이외 immediate로 사용할 수 있는 immediate의 크기는 오직 12bits까지로 제한( 211 2111 )된다는 limitation도 있다. 이 점은 instruction format을 살펴보면 명확히 알 수 있다.

R-format instruction

I-format instruction


2.9 Representing Instructions

RISC-V Instruction-Set Cheatsheet

RISC-V instructions는 모든 instruction이 32bit로 이루어져 있다. RISC-V instruction에는 다양한 formats가 존재한다.

RISC-V instruction format

func: 추가 opcode 필드를 의미한다.

위 그림에 나오지 않은 S-type은 다음과 같다.

S-type

   📝 예제 3: s-type   

sd x5, 240(x10)를 s-type format으로 표현하라.

   🔍 풀이   

offset 240를 32( 25 )로 나누면 몫은 7, 나머지는 16이다.

이를 종합하면 다음과 같다.(x5: 101, x10: 1010, opcode(35): 100011, func3(필드 값 2): 10)

immediate[11:5] rs2 rs1 func3 immediate[4:0] opcode
0000111 01010 00101 010 10000 0100011

2.9.1 addressing mode

instruction에서 oprand의 address를 나타낼 수 있는 방법은 다양하게 있다.(아래 예시 외 다양한 addressing mode가 존재한다.)

RISC-V addressing mode

rs2, rs1 필드 위치를 유지하기 위해, immediate가 상위, 하위 필드로 나뉘어 들어간다.


2.10 Logical Operations

bitwise manipulation을 위한 instruction인 logical operation을 살펴보자.

// C code            // RISC-V instruction
<<  // shift left    // sll(Shift Left Logical), slli
>>  // shift right   // srl(Shift Right Logical), srli
&   // bitwise AND   // and, andi
|   // bitwise OR    // or, ori
^   // bitwise XOR   // xor, xori
~   // bitwise NOT   // xori

R-format일 경우, instruction은 다음과 같은 형태를 갖는다.

R-type logical


2.10.1 Shift Operations

R-format과 I-format 모두 shift operation을 지원한다.


2.10.2 And Operations

(생략)


2.10.3 OR Operations

(생략)


2.10.4 XOR Operations

(생략)