行列:2つのインデックス、1つのアドレス

行列:2つのインデックス、1つのアドレス

行列とは、数値を行と列に並べたグリッドのことです。行とはグリッドの1本の水平な並びで、左から右へ読みます。列とはグリッドの1本の垂直な並びで、上から下へ読みます。メモリには行や列という概念はありません。メモリは1本の長いバイトの並びであり、前のレッスンで扱ったベクトルと同じ種類のものです。行列を格納するには、その行と列をその1本の並びの中に順番に置かなければなりません。通常選ばれるのは行優先順です。つまり、最初の行のすべての数値を格納し、次に2番目の行のすべての数値を格納し、すべての行を置き終えるまで続けます。

2つのインデックス、1つのアドレス

行列における要素の位置は2つのインデックスで与えられます。行インデックスはその要素がどの行に属するかを示し、列インデックスはどの列に属するかを示します。どちらの数え方も0から始まり、ベクトルのレッスンでインデックスが機能したのと同じです。メモリは数値の単一の平坦な並びを保持するため、2つのインデックスはまず1つの平坦化されたインデックスに結合されなければなりません。行インデックスに行列の列数を掛け、その結果に列インデックスを足します。そこから平坦化されたインデックスは、ベクトルのインデックスと同じ方法でバイトアドレスになります。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の冪であるため、この掛け算はシフトで行えましたが、行列の列数は通常そうではないため、ここでは の代わりに が使われます。列インデックスが足され、その結果はもう1つの によって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 レイアウトとあなた自身の読み取りが、上に示した 、、 の演算を使ってその単一の行に課す形です。グリッドと平坦化された行が、2度示された1つのものとして理解されれば、同じパターンがあらゆるデータのグリッドをカバーします。画像のピクセルの行、ゲーム盤、あるいはスプレッドシートです。それぞれは1つの配列であり、1回の余分な掛け算で到達できます。

行列の和

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

2つのネストされたループが行列にインデックスを付けます。外側のループは行を選択し、内側のループはその行内の列を選択します。 は行インデックスに列数を掛けます。列インデックスを足して4でスケーリングすると、その要素のアドレスが得られます。そのアドレスに格納された値がロードされ、実行中の合計に加算されます。内側のループが1つの行を終えると、外側のループは次の行へ移動し、3つの行すべてが処理されるまで続きます。

ℹ
行優先順は、まず1行目のすべての数値を格納し、次に2行目のすべての数値を格納し、そのパターンを続けます。これは規則ではなく慣習です。Fortranを含む他の言語では列優先順を使用します。つまり、まず1列目のすべての数値、次に2列目のすべての数値、というように続きます。データの書き手とデータの読み手は、どちらのレイアウトが使われたかについて合意していなければなりません。RISC-Vは、あなたが計算した任意のアドレスで読み書きします。