RISC-V에서는 Arithmetic/Logical Operation을 지원하는 Instruction이 존재한다.
아래와 같이 RV64I BASE INTEGER Instruction에서 Arithmetic 동작은 아래와 같이 addition/substraction 동작을 수행한다. 기본적으로 각 Instruction은 하나의 동작만 수행하도록 되어있다.
그리고 아래와 같이 각 동작들은 1개의 destination register와 2개의 source 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을 만들었다.
이와 같이 규칙성을 만들어 심플하게 만들면 구현 또한 간단해지며, 구현이 간단해지면 적은 비용으로 높은 성능을 낼 수 있다.
그런데 연산을 수행하다 보면, 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을 구성하였다.
그렇다고 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으로 지정하여 명령어를 만들었다.
그렇다면 하위 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 테이블을 보여준다.
'IT_Study > CS_Study' 카테고리의 다른 글
[Computer Architecture] (4-5) RISC-V Control Transfer Operation (1) | 2024.04.28 |
---|---|
[Computer Architecture] (4-4) RISC-V Data Transfer Operation (1) | 2024.04.28 |
[Computer Architecture] (4-2) RISC-V Register 설명 (0) | 2024.04.27 |
[Computer Architecture] (4-1) RISC-V란 무엇인가? (1) | 2024.04.27 |
[Computer Architecture] (3) Big endian & Little endian (0) | 2024.04.27 |
댓글