Матрицы: два индекса, один адрес

Матрицы: два индекса, один адрес

Матрица — это сетка чисел, расположенных в строках и столбцах. Строка — это одна горизонтальная линия сетки, читаемая слева направо. Столбец — это одна вертикальная линия сетки, читаемая сверху вниз. В памяти нет понятия строк или столбцов. Это одна длинная линия байтов, тот же вид вектора, что рассматривался в предыдущем уроке. Чтобы сохранить матрицу, её строки и столбцы должны быть размещены один за другим в этой единственной линии. Обычный выбор — порядок row-major: сохранить каждое число первой строки, затем каждое число второй строки, и продолжать, пока каждая строка не будет размещена.

Два индекса, один адрес

Позиция элемента в матрице задаётся двумя индексами: индексом строки, к которой он принадлежит, и индексом столбца, к которому он принадлежит. Отсчёт обоих начинается с 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 — степень двойки, но число столбцов матрицы обычно таковой не является, поэтому здесь используется вместо . Индекс столбца прибавляется, а результат масштабируется на 4 с помощью ещё одного (4 — степень двойки, так что сдвиг для этой части всё ещё работает). Это даёт байтовое смещение элемента в заданной строке и столбце.

Индексация по строкам
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. В тот же момент тот же элемент подсвечивается в уплощённой строке ниже, потому что умножение индекса строки на число столбцов и прибавление индекса столбца даёт адрес этого элемента, который совпадает с шестнадцатеричным значением, показанным с обеих сторон. RISC-V работает только с уплощённой линией справа. Сетка — это форма, которую на эту единственную линию накладывают .data-раскладка вашей программы и ваше собственное её чтение, используя арифметику , и , показанную выше. Как только сетка и уплощённая строка понимаются как одна вещь, показанная дважды, тот же шаблон охватывает любую сетку данных: строки пикселей изображения, игровое поле или электронную таблицу. Каждая из них — один массив, доступ к которому осуществляется с одним дополнительным умножением.

Суммирование матрицы

Сложите все элементы матрицы 3 на 3
.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 читает и пишет по любому адресу, который вы вычислите.