본문 바로가기
IT_Study/CS_Study

[Computer Architecture] (4-3) RISC-V Arithmetic / Logical Operation

by 두번째얼룩 2024. 4. 27.

 

RISC-V에서는 Arithmetic/Logical Operation을 지원하는 Instruction이 존재한다. 

아래와 같이 RV64I BASE INTEGER Instruction에서 Arithmetic 동작은 아래와 같이 addition/substraction 동작을 수행한다. 기본적으로 각 Instruction은 하나의 동작만 수행하도록 되어있다. 

addidtion / substraction operation

 

그리고 아래와 같이 각 동작들은 1개의 destination register와 2개의 source register로 구성되어 있다.

operation register

Register의 주소를 나타내므로 register 개수만큼 표현할 수 있는 bit이 필요하다. RISC-V는 32개의 Register를 가지고 있으므로 5-bit으로 충분히 표현 가능하다. 이렇듯 Register로 표현하면, 메모리의 주소를 표현하는 것보다 매우 적은 bit으로 설정가능하다. 이는 Instruction의 크기를 줄일 수 있어 더 빠르고 효과적이다. 

* Register vs Memory

Register는 memory보다 접근 속도가 매우 빠르지만 비용/설계 측면에서 크게 만들기 어렵다.

모든 ALU Instruction은 Memory에 직접 접근할 수 없고, Register를 통해서만 가능하다. 
메모리 접근은 loads/stores 명령으로 가능하다. 
Register의 개수는 한정적이고, 연산 Instruction은 Register를 사용하도록 되어있으므로 이를 효율적으로 관리하는 것이 중요하다. 이 Register optimization은 Compiler가 주로 수행한다. 

이를 아래와 같이 R Type으로 통일시켜 Instruction을 만들었다.

R-type

이와 같이 규칙성을 만들어 심플하게 만들면 구현 또한 간단해지며, 구현이 간단해지면 적은 비용으로 높은 성능을 낼 수 있다. 

 

그런데 연산을 수행하다 보면, source 중 하나가 Constant 인 경우가 종종 있다. 

이를 수행하기 위한 방법에는 여러 가지에 있다. 

첫 번째로는 주로 사용되는 Constants를 메모리에 넣어두고, 이를 로드해서 사용하는 것이다. 

위의 예제에서 '4'를 특정 위치에 넣어두고, 이 정보를 기반으로 '4'를 Register로 읽어와서 연산을 수행하는 것이다.

그런데 Memory 접근은 오래 걸리는 작업으로 성능에 영향을 주기 때문에 적합하지 않다. 

두 번째로는 x0와 같이 hardwired registers를 더 많드는 것이다. 가령 '1'을 만들고, 이를 더하여 사용하는 것이다.

그런데 숫자의 크기에 따라 Instruction 숫자가 늘어난다는 단점이 있다.

마지막 방법으로는 Instruction 자체에 Constant 값을 넣을 수 있도록 만드는 것이다. 

이럴 경우, 1개의 Instruction만으로도 연산이 가능하다. 이 방법에도 단점이 있는데, Instruction에서 Constant를 표현하기 위해 사용가능한 bit 수는 12-bit이다. 해당 필드 이름을 immediate format이라고 하며 줄여서 imm이라고 표현한다. imm은 signed-extended 표현을 사용하므로 -2^11부터 2^11-1 까지만 표현가능하다. 이런 단점이 있지만, 일반적으로 imm이 표현할 수 없는 크거나 작은 수의 연산을 이뤄지기 어렵다. 따라서 이 정도의 구현으로도 충분히 성능향상을 기대할 수 있다.

이를 아래와 같이 I-type으로 통일시켜 Instruction을 구성하였다.

I-type

 

 그렇다고 imm이 표현할 수 없는 Constant 숫자의 연산이라고 해서 위의 2가지 방법처럼 비효율적이지 않다. 32-bit Constant를 만들기 위하여 32번째 bit부터 13번째 bit까지 값을 입력할 수 있는 'lui' (Load Upper immediate) 명령어를 만들었다.  

// Load Upper immediate
lui rd, constant(20-bit) # rd[31:12] = constant

 

이 명령어를 사용하면 상위 20-bit을 설정할 수 있다. 이와 같이 20-bit의 imm을 사용할 수 있게 하는 명령어 타입을 U-type으로 지정하여 명령어를 만들었다. 

U-type

 

그렇다면 하위 12-bit은 어떻게 설정할까? 

바로 'ori' (OR Immediate) 연산을 수행하여 설정할 수 있다. 이를 활용하여 다 표현해보도록 하겠다. 

// Load Upper immediate -> OR immediate
// 32-bit constant = c
lui rd, c[19:0]        # rd[31:12] = c[19:0], rd[11:0] = 0
ori rd, rd, c[11:0]    # rd[11:0] = rd[11:0] | c[11:0] -> rd[11:0] = c[11:0]
                       # rd[31:12] = rd[31:12] | 0     -> rd[31:12] = rd[31:12]

2개의 Instruction을 조합하면 32-bit constant를 만들 수 있다. 

 

RV64M과 같이 Multiply Extension을 사용할 경우에는 Multiply가 지원되지만, RV64I만 사용할 경우, Multiply가 지원되지 않는다. 그렇다면 어떻게 Multiply를 지원할 것인가? 바로 Shift와 Add 명령어의 조합으로 수행 할 수 있다. 

예를 들어 아래와 같은 C 코드가 있다고 하자.

long t4 = y * 48;

이는 아래와 같은 Shift와 Add 명령어 조합으로 구현할 수 있다.

// long t4 = y * 48;
// a1 = y;
// a5 = t4;

slli a5, a1, 1	# a5 = y*2 = 2y
add  a1, a5, a1 # a1 = a5 + y = 2y + y = 3y
slli a5, a1, 4  # a5 = a1*16  = 16*3y = 48y;

잘 살펴보면 shift left를 이용하여 2^n 곱셉을 수행하고, add로 odd 값을 만든다. 

 

 

Logical Operation은 AND, OR, XOR, NOT, Shift left, Shift right(arithmetic), Shift right(logical)로 구성되어 있다. 

여기서 AND, OR, XOR, NOT은 bit-by-bit 형태로 수행된다. 

일반적으로 Mask를 수행할 때는 AND를, Bit을 추가할 때는 OR, 서로 다른 부분을 찾을 때는 XOR를 사용한다.

예를 들어 아래와 같은 C 코드가 있다고 하자.

// mask = 1111_1001
long mask = (1 << 8) - 7;
long rval = t2 & mask;

 

mask는 Constant 값으로 Compiler가 미리 계산을 수행할 수 있다. 이를 andi inst.으로 표현하면 아래와 같다.

// 249 = (1 << 8) - 9
// a0 = t2
andi a0, a0, 249

 

 

Shift right(arithmethic)은 Sign-bit을 고려해서 shift를 수행하는 것이고, Shift right(logical)은 '0'을 채우면서 shift를 수행한다는 차이점이 있다. 

아래는 위에서 다루지 않은 Arithmetic operation/Logicl operation 테이블을 보여준다.

Arithmetic Operation Table
Logical Operation Table

 

 

 

 

 

댓글