add a, b, c -> a = b + c
sub a, b, c, -> a = b - c
destination operand를 가장 앞에 둔다.
기능을 디자인할 때는 단순한 표준을 따른다.
예를 들어 add명령어와 sub명령어를 비슷한 포맷으로 제작해야 이해하기 쉬워진다. 또한 기능을 작은 단위로 나누어 한 명령 당 한 기능을 수행하게 한다.
어셈블리 명령어는 레지스터에 있는 데이터만 연산할 수 있다. RISC-V에서 각 레지스터는 32비트의 크기를 지닌다.
ex) add x5, x6, x7에서 오퍼렌드는 모두 레지스터를 뜻하며 각각 32비트 크기이다.
이 경우 명령어는 x6과 x7레지스터에 할당된 값을 더해서 x5메모리에 적재한다고 해석할 수 있다.
데이터 크기에 따른 명칭
32-bit : word
64-bit : doubleword
- 메모리 주소는 byte단위로 저장된다. 따라서 1, 2, 3...이 아니라 0, 4, 8...순서이다. 하지만 RISC-V에서는 예외적으로 1, 5, 9...로 저장될 수도 있다. (이 경우 성능 하락 가능성 있음)
명령어가 DRAM(메인 메모리) 대신 32비트로 크기가 작은 레지스터를 이용하는 이유는 속도가 빠르기 때문이다.
데이터 이동
Load(lw): 메모리의 데이터를 사용하기 위해 레지스터로 이동
Store(sw): 레지스터의 결과값을 메모리에 저장
따라서 메모리 상에 있는 데이터를 이용해 연산하려면 다음 절차가 필요하다.
요구하는 식 : x22[12] = x9 + x22[8]
기능하는 명령어 :
lw x9, 32(x22)
add x9, x21, x9
sw x9, 48(x22)
코드를 수행하는 동안 x22레지스터의 8번 인덱스(접근하기 위해 8*4=32 오프셋 필요)의 값을 load하고 덧셈한 뒤 x22레지스터의 12번 인덱스에 store하는 작용이 일어났다.
컴퓨터가 연산을 수행하려면 레지스터가 필요하지만 대부분의 프로그램에는 32개보다 많은 변수가 존재한다. 따라서 당장 사용하지 않는 데이터는 DRAM에 적재해야 하는데 이것을 spilling이라고 부른다. DRAM에 접근하는 것 자체가 속도 저하를 초래하기 때문에 spilling을 최소화하는 것이 좋다.
n-bit 숫자가 있을 때 각 자리에 해당하는 weight를 곱해 나타낸다.
예를 들어 1011이라는 binary number가 있다면,
12^3 + 02^2 + 02^1 + 02^0 으로 변환할 수 있다.
계산법
2의 보수에서 덧셈하다가 비트 수가 넘치면 무시해도 된다.
뺄셈은 음수를 더하는 식으로 진행한다.
모든 비트를 만대로 바꾼 뒤에 1을 더한다.
sign bit를 숫자 왼쪽에 8개 추가한다.addi x22, x22, 4
Immediate 명령어를 사용하면 레지스터 값에 곧바로 연산한다.
메모리에 접근하는 명령어를 하나 줄일 수 있다. 명령어가 늘어나면서 하드웨어 동작이 복잡해질 수 있지만, 레지스터에 작은 상수를 더하는 동작은 워낙 자주 쓰이기 때문에 addi를 사용하면 성능을 개선할 수 있다.
그럼 상수끼리의 연산은?
상수+상수 정도의 간단한 연산은 하드웨어 단까지 오지 않고 컴파일 단계에서 처리된다.
교과서 2.8 문제
1. addi x30, x10, 8
여기에서 x10이 배열 A의 base address라면, x10+8은 A의 두 번째 요소(의 주소)를 불러오라는 의미이다.
-> x30 = &A[2]
2. addi x31, x10, 0
&A = &A[0]
-> x31 = &A
3. sw x31, 0(x30)
-> A[2] = &A
4. lw x30, 0(x30)
-> x30 = &A (=A[2])
5. add x5, x30, x31
-> x5 = &A + &A = 2(&A)
C식 코드 -> 어셈블리로 변환
// 코드
int *A, *B;
B = A + 1;
// 어셈블리로 변환
addi x11, x10, 4