[[Arm]]
-How to transpose a 4x4 matrix of 32bit value using NEON
-NEONを使って32bitの4x4行列の転置を行う話
*転置命令とは [#w6b44954]
-NEON にはVTRN命令があり、行列に見立てたQレジスタの、中身を入れ替えることができる¬e{arm-processor-rearrange:[[ARM Processors: Coding for NEON - Part 5: Rearr... | ARM Connected Community>https://community.arm.com/groups/processors/blog/2012/03/13/coding-for-neon--part-5-rearranging-vectors]], 2012-03-13公開, 2016-07-07閲覧};
-例えば、16bitの要素8個のベクトル2つを使って、以下の様なデータを考える
| 7| 6| 5| 4| 3| 2| 1| 0|
q0 | 11| 12| 13| 14| 15| 16| 17| 18|
q1 |206|205|204|203|202|201|200|199|
-16bit幅のVTRN命令を使うと、16x8のベクトル2本を、2x2の行列4個と見立てて転置が行える(vtrnq_u16)
-&mimetex(\begin{pmatrix} 11 &12 \\ 206&205 \end{pmatrix} \begin{pmatrix} 13 &14 \\ 204&203 \end{pmatrix} \begin{pmatrix} 15 &16 \\ 202&201 \end{pmatrix} \begin{pmatrix} 17 &18 \\ 200&199 \end{pmatrix}); -> &mimetex(\begin{pmatrix} 205 &12 \\ 206&11 \end{pmatrix} \begin{pmatrix} 203 &14 \\ 204&13 \end{pmatrix} \begin{pmatrix} 201 &16 \\ 202&15 \end{pmatrix} \begin{pmatrix} 199 &18 \\ 200&17 \end{pmatrix});
after VTRN instruction (vtrnq_u16)
| 7| 6| 5| 4| 3| 2| 1| 0|
q0 |205| 12|203| 14|201| 16|199| 18|
q1 |206| 11|204| 13|202| 15|200| 17|
*4x4行列の転置(16bit) [#beec1992]
-例えば16bitの4x4行列があって、4本のDレジスタに入っている場合、以下の手順で転置できる
| 3| 2| 1| 0|
d0 |207| 11|100|999|
d1 |206| 12|101|998|
d2 |205| 13|102|997|
d3 |204| 14|103|996|
-d0とd1にVTRN命令(16bit幅)を使う
after VTRN d0 d1
| 3| 2| 1| 0|
d0 | 12| 11|998|999|
d1 |206|207|101|100|
d2 |205| 13|102|997|
d3 |204| 14|103|996|
-d2とd3でVTRN命令(16bit幅)を使う
after VTRN d2 d3
| 3| 2| 1| 0|
d0 | 12| 11|998|999|
d1 |206|207|101|100|
d2 | 14| 13|996|997|
d3 |204|205|103|102|
-q0とq1でVTRN命令(32bit幅)を使う。ArmのVレジスタはn番目のqレジスタはn*2番目とn*2+1番目のdレジスタを連結したもの
-なのでq0とq1の転置は、(d0とd1)と(d2とd3)での転置になる
after VTRN q0 q1
| 3| 2| 1| 0|
d0 |996|997|998|999|
d1 |103|102|101|100|
d2 | 14| 13| 12| 11|
d3 |204|205|206|207|
*4x4行列の転置(32bit) [#xed3744d]
-同様に32bitの転置をしたい場合は多々ある
-しかし、32bitでやる場合、64bit幅のVTRN命令が必要になり、Armでは実現できない。
-という訳で、多少の工夫が必要となる
**アセンブリで書く場合 [#t42c6ff5]
-参考ページ¬e{arm-transpose-asm:[[Matrix transpose with NEON | ARM Connected Communit>https://community.arm.com/message/7916#7916]], 2011-10-03公開, 2016-07-07閲覧};に書いてあるように、VSWP命令を使うと、4ステップで実行可能
#geshi(asm){{
vtrn.32 q0, q1
vtrn.32 q2, q3
vswp d1, d4
vswp d3, d6
}}
-1ステップずつ書くと、各qレジスタに4つずつfloatが入っていたとして
| 3| 2| 1| 0|
q0 | 0.1| 11.0|100.0|999.0|
q1 | 0.2| 12.0|101.0|998.0|
q2 | 0.3| 13.0|102.0|997.0|
q3 | 0.4| 14.0|103.0|996.0|
-最初のVTRN.32の後
after VTRN.32 q0 q1
| 3| 2| 1| 0|
q0 | 12.0| 11.0|998.0|999.0|
q1 | 0.2| 0.1|101.0|100.0|
q2 | 0.3| 13.0|102.0|997.0|
q3 | 0.4| 14.0|103.0|996.0|
-2回めのVTRN.32の後
after VTRN.32 q2 q3
| 3| 2| 1| 0|
q0 | 12.0| 11.0|998.0|999.0|
q1 | 0.2| 0.1|101.0|100.0|
q2 | 14.0| 13.0|996.0|997.0|
q3 | 0.4| 0.3|103.0|102.0|
-VSWPの後。d1がq0の後ろ(左半分)、d4がq2の前半(右半分)である点に注意
after VSWP d1 d4
| 3| 2| 1| 0|
q0 |996.0|997.0|998.0|999.0|
q1 | 0.2| 0.1|101.0|100.0|
q2 | 14.0| 13.0| 12.0| 11.0|
q3 | 0.4| 0.3|103.0|102.0|
-VSWPの後。d3がq1の後ろ(左半分)、d6がq3の前半(右半分)である点に注意
after VSWP d3 d6
| 3| 2| 1| 0|
q0 |996.0|997.0|998.0|999.0|
q1 |103.0|102.0|101.0|100.0|
q2 | 14.0| 13.0| 12.0| 11.0|
q3 | 0.4| 0.3| 0.2| 0.1|
-これで、無事32bitの転置に成功した(転置なのでintだろうとfloatだろうと関係ない点にも留意)
-しかし、これをC/C++のコードでから呼ぼうとすると、ちょっと困ったことになる。
#geshi(c++){{
asm(
"VMOV q0 %4\n\t" // Copy input to q0
"VMOV q1 %5\n\t" // Copy input to q1
"VMOV q2 %6\n\t" // Copy input to q2
"VMOV q3 %7\n\t" // Copy input to q3
"vtrn.32 q0, q1\n\t"
"vtrn.32 q2, q3\n\t"
"vswp d1, d4\n\t"
"vswp d3, d6\n\t"
"VMOV %0 q0\n\t" // write back q0
"VMOV %1 q1\n\t" // write back q1
"VMOV %2 q2\n\t" // write back q2
"VMOV %3 q3\n\t" // write back q3
: "=r" (transposed_v0),"=r" (transposed_v1),"=r" (transposed_v2),"=r" (transposed_v3)
: "r" (src_v0),"r" (src_v1),"r" (src_v2),"r" (src_v3)
: "q0","q1","q2","q3"
);
}}
-このように、たった4命令のはずが、Qレジスタ8個、合計12命令も使う処理に変わってしまう。
-なんでやねん。
-理由は、qレジスタとそれに対応する2つのdレジスタを指定する方法が無いため
-当然、もしそんな方法があるんだったら手島に知らせてください。
-このアセンブラでの方法のメリットは、dレジスタの境を超えてコピーする際にVSWPを使って入れ替えていること
-しかし、そのためにはqレジスタとdレジスタをコンパイラ任せではなく、自分で指定する必要がある
-そのため残念ながら、C/C++で書いたコードと、ASMの間でレジスタのコピーが必要になり、せっかくの簡単な転置も、なんだか無駄に長くなってしまう。
-しかも、ASMで書いてしまったので、このコードはAarch64(64bitArm)用にはビルドできなくなってしまう。¬e{hoge:厳密に言えばコンパイルは通るが、オブジェクトにできない};
-なので、32bitArm固定で、全ASMで書くことができる環境であれば、これが最速である
**intrinsicを使う方法 [#w63025f6]
-Arm32bit/64bitの両方でビルドできるC/C++コードを考えると、コンパイラが提供するintrinsicを使うのが一番安全であろう
-ここではgccに付属のintrinsicで説明する
#geshi(c++){{
float32x4_t _v0 = vcvtq_f32_u32(vld1_u8(src_v0));
float32x4_t _v1 = vcvtq_f32_u32(vld1_u8(src_v1));
float32x4_t _v2 = vcvtq_f32_u32(vld1_u8(src_v2));
float32x4_t _v3 = vcvtq_f32_u32(vld1_u8(src_v3));
// | 3| 2| 1| 0|
// _v0| 0.1| 11.0|100.0|999.0|
// _v1| 0.2| 12.0|101.0|998.0|
// _v2| 0.3| 13.0|102.0|997.0|
// _v3| 0.4| 14.0|103.0|996.0|
float32x4x2_t v01 = vtrnq_f32(_v0, _v1);
float32x4x2_t v23 = vtrnq_f32(_v2, _v3);
// | 3| 2| 1| 0|
// _v0| 12.0| 11.0|998.0|999.0|
// _v1| 0.2| 0.1|101.0|100.0|
// _v2| 14.0| 13.0|996.0|997.0|
// _v3| 0.4| 0.3|103.0|102.0|
float32x4_t _dst0 = vcombine_f32(vget_low_f32(v01.val[0]), vget_low_f32(v23.val[0]));
float32x4_t _dst1 = vcombine_f32(vget_low_f32(v01.val[1]), vget_low_f32(v23.val[1]));
float32x4_t _dst2 = vcombine_f32(vget_high_f32(v01.val[0]), vget_high_f32(v23.val[0]));
float32x4_t _dst3 = vcombine_f32(vget_high_f32(v01.val[1]), vget_high_f32(v23.val[1]));
// | 3| 2| 1| 0|
// _v0|996.0|997.0|998.0|999.0|
// _v1|103.0|102.0|101.0|100.0|
// _v2| 14.0| 13.0| 12.0| 11.0|
// _v3| 0.4| 0.3| 0.2| 0.1|
}}
-このように、float32x4_tでなく、float32x4x2_tを使うのがミソ
-最後の4行に、鬼のようにvget_low_f32、vget_high_f32やvcombine_f32が出てくるが、こいつらはコンパイル時にq0からd0を参照、あるはその逆、の様に動いてくれるので見た目より命令数はずっと少なくて済む。
**RGBを読み込む場合の転置 [#m1758bd3]
-通常の行列操作以外にも、インターリーブされたRGBを紐解くのに、転置が便利だったりする
-RGBは通常8bitだが、それをfloat演算に用いる場合に上記の様な転置が必要になる
-ただし、そもそもRGBの8bitずつをロードしたい場合は、VLD3命令を使うと、ロードするだけでインターリーブが解除される¬e{arm-processor-interleave:[[ARM Processors: Coding for NEON - Part 1: Load ... | ARM Connected Community>https://community.arm.com/groups/processors/blog/2010/03/17/coding-for-neon--part-1-load-and-stores]], 2010-03-17公開, 2016-07-07閲覧};