행렬: 두 개의 인덱스, 하나의 주소

행렬: 두 개의 인덱스, 하나의 주소

행렬은 숫자를 행과 열로 배열한 격자입니다. 행은 격자의 가로 한 줄이며 왼쪽에서 오른쪽으로 읽습니다. 열은 격자의 세로 한 줄이며 위에서 아래로 읽습니다. 메모리에는 행이나 열이라는 개념이 없습니다. 메모리는 하나의 긴 바이트 나열이며, 이전 레슨에서 다룬 벡터와 같은 종류입니다. 행렬을 저장하려면 그 행과 열을 그 하나의 나열 안에 차례대로 놓아야 합니다. 일반적으로 선택되는 것은 행 우선 순서입니다. 즉, 첫 번째 행의 모든 숫자를 저장하고, 그다음 두 번째 행의 모든 숫자를 저장하며, 모든 행을 다 놓을 때까지 계속합니다.

두 개의 인덱스, 하나의 주소

행렬에서 원소의 위치는 두 개의 인덱스로 주어집니다. 행 인덱스는 그 원소가 어느 행에 속하는지를 나타내고, 열 인덱스는 어느 열에 속하는지를 나타냅니다. 둘 다 0부터 세기 시작하며, 벡터 레슨에서 인덱스가 작동한 방식과 같습니다. 메모리는 숫자의 단일한 평탄한 나열을 담으므로, 두 인덱스는 먼저 하나의 평탄화된 인덱스로 결합되어야 합니다. 행 인덱스에 행렬의 열 수를 곱한 다음, 열 인덱스를 더합니다. 거기서 평탄화된 인덱스는 벡터의 인덱스와 같은 방식으로 바이트 주소가 됩니다. address = base + index * 4.

행과 열을 주소로 평탄화하기
mul t4, t1, t3 # t4 = 행 인덱스 * 열 개수
add t4, t4, t2 # t4 = that + 열 인덱스(평탄화된 인덱스)
slli t4, t4, 2 # t4 = 바이트 오프셋 = 인덱스 * 4
add t5, t0, t4 # t5 = 해당 행과 열에 있는 요소의 주소
lw t5, 0(t5) # t5 = 그 주소의 값

는 행 인덱스에 열 수를 곱합니다. 벡터 레슨에서는 4가 2의 거듭제곱이므로 이 곱셈을 시프트로 할 수 있었지만, 행렬의 열 수는 보통 그렇지 않으므로 여기서는 대신 이 사용됩니다. 열 인덱스가 더해지고, 그 결과는 또 다른 로 4만큼 스케일됩니다(4는 2의 거듭제곱이므로 이 부분에는 여전히 시프트가 통합니다). 그러면 주어진 행과 열에 있는 원소의 바이트 오프셋이 나옵니다.

행 우선 인덱싱
index=0× 3 +0=0
1
0x10010000
2
0x10010004
3
0x10010008
4
0x1001000c
5
0x10010010
6
0x10010014
7
0x10010018
8
0x1001001c
9
0x10010020
01
0x10010000
12
0x10010004
23
0x10010008
34
0x1001000c
45
0x10010010
56
0x10010014
67
0x10010018
78
0x1001001c
89
0x10010020

행 0, 열 0은 인덱스 0에 대응합니다.

왼쪽의 격자는 행렬이 종이에 어떻게 그려지는지를 보여 줍니다. 오른쪽의 단일 행은 그 같은 숫자들이 메모리에 어떻게 배치되는지를 보여 줍니다. 하이라이트는 격자를 읽기 순서로 이동합니다. 행 0을 왼쪽에서 오른쪽으로, 그다음 행 1, 그다음 행 2입니다. 동시에 아래의 평탄화된 행에서도 같은 원소가 켜집니다. 행 인덱스에 열 수를 곱하고 열 인덱스를 더하면 그 원소의 주소가 나오며, 이것이 양쪽에 표시된 16진수 값과 일치하기 때문입니다. RISC-V는 오른쪽의 평탄화된 행만 다룹니다. 격자란 프로그램의 .data 레이아웃과 여러분 자신의 읽기가 위에 나온 , , 산술을 사용해 그 단일 행에 부여하는 형태입니다. 격자와 평탄화된 행이 두 번 표시된 하나의 것으로 이해되면, 같은 패턴이 모든 데이터 격자를 포괄합니다. 이미지의 픽셀 행, 게임판, 또는 스프레드시트입니다. 각각은 하나의 배열이며, 한 번의 추가 곱셈으로 도달합니다.

행렬의 합 구하기

3x3 행렬의 모든 원소를 더하세요
.data
m: .word 1, 2, 3, 4, 5, 6, 7, 8, 9 # 3x3 행렬, 행 단위로 저장됨
.text
la t0, m # 기준 주소
li t1, 3 # 행 개수
li t2, 3 # 열 개수
li t3, 0 # 현재 행 인덱스, 0에서 시작
li s0, 0 # 누적 합계
row_loop:
beq t3, t1, row_done
li t4, 0 # 현재 열 인덱스, 0에서 시작
col_loop:
beq t4, t2, col_done
mul t5, t3, t2 # 행 인덱스 * 열 개수
add t5, t5, t4 # + 열 인덱스 = 평탄화된 인덱스
slli t5, t5, 2 # 바이트 오프셋 = 인덱스 * 4
add t6, t0, t5 # 해당 행과 열에 있는 요소의 주소
lw t6, 0(t6) # 그 주소의 값
add s0, s0, t6 # 합계 += 요소
addi t4, t4, 1 # 다음 열로 이동
j col_loop
col_done:
addi t3, t3, 1 # 다음 행으로 이동
j row_loop
row_done:
mv a0, s0 # 합계 출력: 1+2+...+9 = 45
li a7, 1
ecall
li a7, 10
ecall

두 개의 중첩 루프가 행렬을 인덱싱합니다. 바깥 루프는 행을 선택하고 안쪽 루프는 그 행 안의 열을 선택합니다. 는 행 인덱스에 열 수를 곱합니다. 열 인덱스를 더하고 4로 스케일하면 그 원소의 주소가 나옵니다. 그 주소에 저장된 값이 로드되어 실행 중인 합계에 더해집니다. 안쪽 루프가 한 행을 끝내면 바깥 루프는 다음 행으로 이동하며, 3개의 행이 모두 처리될 때까지 계속합니다.

ℹ
행 우선 순서는 첫 번째 행의 모든 숫자를 저장한 다음, 두 번째 행의 모든 숫자를 저장하고, 그 패턴을 계속합니다. 이것은 규칙이라기보다 관례입니다. Fortran을 포함한 다른 언어들은 열 우선 순서를 사용합니다. 즉, 첫 번째 열의 모든 숫자, 그다음 두 번째 열의 모든 숫자, 이런 식으로 계속됩니다. 데이터를 쓰는 사람과 데이터를 읽는 사람은 어떤 레이아웃이 사용되었는지에 대해 합의해야 합니다. RISC-V는 당신이 계산한 주소에서 읽고 씁니다.