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)
  • \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} -> \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)

  • 例えば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

*1  ARM Processors: Coding for NEON - Part 5: Rearr... | ARM Connected Community, 2012-03-13公開, 2016-07-07閲覧
*2  Matrix transpose with NEON | ARM Connected Communit, 2011-10-03公開, 2016-07-07閲覧
*3  厳密に言えばコンパイルは通るが、オブジェクトにできない
*4  ARM Processors: Coding for NEON - Part 1: Load ... | ARM Connected Community, 2010-03-17公開, 2016-07-07閲覧

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-08-24 (木) 09:45:00 (87d)