ようやくコンパイラにNEONのクワッドワードのレジスタを使うコードを吐き出してもらえるようになったので、わずかにハードルを上げてみたいと思います。当初から予定通りで、内積計算ですね。ベクトルの要素毎に単純な掛け算をしているのと比べると、それらの和をとっていかなければならないので、計算はちょっとだけ複雑。それに何と言っても結果はスカラー。コンパイラはどのように料理してくれるのでしょうか。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
例によって、今回対象のCの関数のソースをまず掲げます。毎度、変わり方が微妙で、なにほども進歩していないように見えるソースコードであります。
float innerProd(const int vlen, float* __restrict x, float* __restrict y) { float result = 0.0; for(int i = 0; i < 4*vlen; i++) { result += x[i] * y [i]; } return result; }
前回の反省どおり、コンパイラがベクトルの長さは常に4の倍数に決まっていると「理解」できるようにしてみました。しかし、今回は、掛けた結果を足し込んでいかないとならないので、最後の結果はスカラー値1個にせねばなりません。そんな場合もコンパイラは塩梅よく始末してくれるんでしょうか。まあ、論より証拠で、コンパイルしてみましょう。コンパイルオプションは前回と同じであります。
000105f0 <innerProd>: 105f0: e1a00100 lsl r0, r0, #2 105f4: e3500000 cmp r0, #0 105f8: da00000b ble 1062c <innerProd+0x3c> 105fc: e0820100 add r0, r2, r0, lsl #2 10600: f2c00050 vmov.i32 q8, #0 ; 0x00000000 10604: f4624a8d vld1.32 {d20-d21}, [r2]! 10608: e1500002 cmp r0, r2 1060c: f4612a8d vld1.32 {d18-d19}, [r1]! 10610: f2440df2 vmla.f32 q8, q10, q9 10614: 1afffffa bne 10604 <innerProd+0x14> 10618: f2400da1 vadd.f32 d16, d16, d17 1061c: f3400da0 vpadd.f32 d16, d16, d16 10620: ee103b90 vmov.32 r3, d16[0] 10624: ee003a10 vmov s0, r3 10628: e12fff1e bx lr 1062c: e3a03000 mov r3, #0 10630: ee003a10 vmov s0, r3 10634: e12fff1e bx lr
いやー、結構いい感じにコンパイルしてくれているんじゃないでしょうかね。今回は1命令づつ勉強させていただきましょう。まずは、最初の
lsl r0, r0, #2
例のArmのABI規則により、r0レジスタには第1引数の値が入っている筈。それを2ビット左シフト、即ち4倍しておりますな。まさにこれは、以下の計算に違いない。
4*vlen
その4倍した結果(ベクトルの要素数)を0と比べています。
cmp r0, #0
比べた結果が、0か0より小さい(データを符号付きと解釈)ならば0x1062C番地に分岐しています。
ble 1062c
これは、もしvlenに0やマイナスの値を突っ込まれてしまった場合、何もしない、ということを保証してくれていますな。計算する場合には真っすぐ下に落ちるので、そちらから見ていきます。
add r0, r2, r0, lsl #2
再びフレキシブル第2オペランドが活躍しています。r0を左2ビットシフト(4倍)したものをr2(2番目のベクトルの先頭番地を指している筈)に加えたものをr0に格納しています。直前のr0の値は先ほど見た通りで 4*vlenでしたから、ベクトルの要素長さ、それを4倍(1要素は4バイト)しているので、バイトで数えたベクトル長さを加算していることになります。これは、ベクトルの末尾の番地ですな。
次にq8というクワッドレジスタをゼロクリアしています。
vmov.i32 q8, #0
型は32ビット整数型ですが、ゼロクリアしたことに変わりはなしと。このq8は積算の中間結果を保持しておくためのものでしょう。
次から本体処理の先頭です。
vld1.32 {d20-d21}, [r2]!
このvld1.32 {d20-d21}, [r2]!という書き方は結構まどろっこしく感じますが、1要素が32ビットの「構造体」をダブルワード(64ビット)幅のレジスタd20とd21に、r2レジスタが保持しているアドレスからロード(64ビット2本なので、128ビット=16バイト)、そして最後のビックリマークでr2にロード幅分のアドレス増分16を加えて次回ロードの準備をしておく、というもの。
cmp r0, r2
その直後に、先ほど出てきた r0と更新したばかりのr2を比べています。もし、r2がr0と一致するならば、末尾である筈。cmp命令でフラグに結果は残りますが、ここでは一端とっておくようです。まだ、やる仕事あるから。cmpを先にしているのは、多分、ロードを2回続ける隙間の時間でcmpは終わってしまうからと推測します。次の命令で、もう一方のベクトルの値4要素16バイトをロード。
vld1.32 {d18-d19}, [r1]!
ここで、d18-d19は、実はq9と同じもの。d20-d21はq10と同じもの。
vmla.f32 q8, q10, q9
さすれば、上のvmla.f32は、q10とq9をvmlaして、q8(積算用に準備した中間レジスタ)にいれる。さて、vmlaとは、
ベクタ積和命令
であります。コンテンポラリーなプロセッサには必ず入っているかと思います。要素毎掛け算した結果を、積算レジスタの保持する値に足し込んでいく、1命令で2度オイシイもの。
bne 10604
そして上記の分岐で、ne=ノット・イコールであるならば、先ほどのループ先頭に戻れ、と。こうしてループを回っていく毎に、4要素ずつ同時に掛け算して、加算されていくわけでした。しかし、最終結果はスカラーであります。その始末は、bneを下に落ちた(つまりイコール、r0がr2と一致した)ときに。
vadd.f32 d16, d16, d17
先ほどの逆で、q8というクワッドワードレジスタは、d16とd17の2本に分かれるのでした。だから、d16とd17を足した結果をd16に入れれば、上半分2要素と下半分2要素をば、足したことになる。これで4要素が2要素になりました。
しかし、次の
vpadd.f32 d16, d16, d16
は何か? Arm社の説明を引用させていただきましょう。
vpadd (ベクタペアワイズ加算)は、2 つのベクタの隣接する要素のペアを加算し、その結果をデスティネーションベクタに返します。
ぶっちゃけ、d16の上要素と下要素を足し込む計算。結果は下に入る筈。
そして、またちょっとまどろっこしいですが、
vmov.32 r3, d16[0]
まず、d16[0]つまり、d16の下要素を、レジスタr3へ転送(取り出し)て、
vmov s0, r3
その値をs0レジスタ(スカラーの浮動小数戻り値ならばここに入れるお約束)に入れている。
そして以下で関数から戻る、と。
bx lr
その下は、もうお分かりでしょう。最初のチェックではねられたvlenの場合は、0をもって返る、と。
めでたしめでたし。