Arm
- How to transpose a 4x4 matrix of 32bit value using NEON
- NEONを使って32bitの4x4行列の転置を行う話
転置命令とは †
- NEON にはVTRN命令があり、行列に見立てたQレジスタの、中身を入れ替えることができる*1
- 例えば、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)
->
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) †
- 例えば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) †
- 同様に32bitの転置をしたい場合は多々ある
- しかし、32bitでやる場合、64bit幅のVTRN命令が必要になり、Armでは実現できない。
- という訳で、多少の工夫が必要となる
アセンブリで書く場合 †
- 参考ページ*2に書いてあるように、VSWP命令を使うと、4ステップで実行可能
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++のコードでから呼ぼうとすると、ちょっと困ったことになる。
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)用にはビルドできなくなってしまう。*3
- なので、32bitArm固定で、全ASMで書くことができる環境であれば、これが最速である
intrinsicを使う方法 †
- Arm32bit/64bitの両方でビルドできるC/C++コードを考えると、コンパイラが提供するintrinsicを使うのが一番安全であろう
- ここではgccに付属のintrinsicで説明する
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を読み込む場合の転置 †
- 通常の行列操作以外にも、インターリーブされたRGBを紐解くのに、転置が便利だったりする
- RGBは通常8bitだが、それをfloat演算に用いる場合に上記の様な転置が必要になる
- ただし、そもそもRGBの8bitずつをロードしたい場合は、VLD3命令を使うと、ロードするだけでインターリーブが解除される*4
|