矩阵:两个索引,一个地址

矩阵:两个索引,一个地址

矩阵是按行和列排列的数字网格。行是网格中的一条水平线,从左到右读取。列是网格中的一条垂直线,从上到下读取。内存没有行或列的概念。它是一长串字节,与上一课介绍的向量是同一种东西。要存储矩阵,它的行和列必须依次放入这一条线中。通常的选择是行优先顺序:先存储第一行的每个数字,再存储第二行的每个数字,一直继续到每一行都被放置完毕。

两个索引,一个地址

元素在矩阵中的位置由两个索引给出:行索引表示它属于哪一行,列索引表示它属于哪一列。两者的计数都从 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 的幂,这个乘法可以用移位完成,但矩阵的列数通常不是 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 行。与此同时,下方扁平化行中的同一个元素也会亮起,因为将行索引乘以列数再加上列索引就得到该元素的地址,这与两侧显示的十六进制值一致。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 在你计算出的任何地址上进行读写。