18
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりNEONAdvent Calendar 2020

Day 2

NEONを実際に書いてみる

Last updated at Posted at 2020-12-01

はじめに

実装例

main.cpp
#include <arm_neon.h>
#include <iostream>

int main(int argc, char** argv)
{
    unsigned char src0[] = {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15};
    unsigned char src1[] = {100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115};
    unsigned char dst [] = {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0};
    uint8x16_t s0 = vld1q_u8(src0);
    uint8x16_t s1 = vld1q_u8(src1);
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
    vst1q_u8(dst, d);
    for( auto i = 0;i < 16;i++)
    {
        std::cout << i << '\t' << (int)src0[i] << '\t' << (int)src1[i] << '\t' << (int)dst[i] << std::endl;
    }
    return 0;
}
  • 単一のファイルなので横着してコマンドベタ書きします
build(ARmv7)
g++ -mfpu=neon main.cpp
  • Arm v7 では、コンパイラに明示的にNEONを使うように指定する必要があります(-mfpu=neon)
  • Arm v8 では、基本命令セットに入っているので、指定する必要はありません
build(ARmv8)
g++ main.cpp
  • 実行結果
0       0       100     100
1       1       101     102
2       2       102     104
3       3       103     106
4       4       104     108
5       5       105     110
6       6       106     112
7       7       107     114
8       8       108     116
9       9       109     118
10      10      110     120
11      11      111     122
12      12      112     124
13      13      113     126
14      14      114     128
15      15      115     130
  • 2つのベクトルに入っている要素が足されていることが分かります。
  • 実際に加算処理はvaddq_u8の1回しか行われていないのですが、16個のデータが一度に処理されています

解説

include文

#include <arm_neon.h>
  • 基本的な命令はこいつをincludeするだけですべて使えるようになります
  • GCCの場合、だいたい /usr/lib/gcc/aarch64-linux-gnu/<GCCのバージョン>/include/arm_neon.h
  • Raspberry Pi 4の場合は /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h

データ型について

  • コード中、uint8x16_tという型を使っています
    • <データ型>x<レーン数>_tというフォーマットで指定します。
    uint8x16_t s0 = vld1q_u8(src0);
  • 基本的な型は以下の9つです
型名 各要素の型 レーン数
uint8x16_t uint8_t 16
uint16x8_t uint16_t 8
uint32x4_t uint32_t 4
uint64x2_t uint64_t 2
int8x16_t int8_t 16
int16x8_t int16_t 8
int32x4_t int32_t 4
int64x2_t int64_t 2
float16x8_t float16_t 8
float32x4_t float32_t 4
float64x2_t float64_t 2
  • 例えばuint16は16bit幅の符号なし整数なので、一般的にunsigned shortと呼ばれる型です。
    • uint16x8_tは、unsigned short8個分がまとまって処理できます
  • 上の9つの型は情報量として128bit分保持できます。
  • Arm v8 では128bitレジスタを使い、Arm v7 では64bitレジスタ2つをくっつけて128bitレジスタとして振る舞います
  • また、全型は、その半分の64bit幅のデータ型が存在します
型名 各要素の型 レーン数
uint8x8_t uint8_t 8
uint16x4_t uint16_t 4
uint32x2_t uint32_t 2
uint64x1_t uint64_t 1
int8x8_t int8_t 8
int16x4_t int16_t 4
int32x2_t int32_t 2
int64x1_t int64_t 1
float16x4_t float16_t 4
float32x2_t float32_t 2
float64x1_t float64_t 1
  • 注意が必要なのは、Arm v7では、doubleの演算はサポートされていません。そのため、Arm v7(もしくはRaspberry Pi 3 + 32bit Raspbianのような32bit OS on Arm v8)では、float64x2_tfloat64x1_tは利用できません。
  • また、float16x8_tfloat16x4_tNEONFP16拡張であり、Armv7では、別途別フラグを確認する必要があります。(詳しくは後日解説)
  • また、poly8x16_tpoly16x8_tpoly64x2_tという「多項式型」と言うのも存在します。
    • が、有効な利用方法及び原理を筆者が理解していないため、今回のアドベントカレンダーでは全面的に割愛します
    • Githubで検索しても、引っかかるのはGCCのテストコードが何千ファイルと同じものが引っかかるだけで、具体的に利用している人をGithub上で見たことはありません。
    • 多分CRCSHA1などを計算するのに必要っぽいのだけれど、不明です。

組み込みSIMD命令について

  • サンプルコードでは vaddq_u8という命令を使いました。
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
  • 各命令は接頭辞のvで始まり、v<命令><bit幅>_<データ型>というフォーマットで表されます。
    • 末尾のデータ型は、引数によって変わり、以下の14種類があります
    • 符号なし整数: u8u16u32u64
    • 符号あり整数: s8s16s32s64
    • 浮動小数点型: f16f32f64
    • 多項式型: p8p16p64
  • 今回はadd命令で、128bit分のデータを利用しました。この場合は末尾にqが付きます。
    • 64bit幅分の同様の命令はvadd_u8です
    • 基本的に64bit幅でも128bit幅でも、演算の内容は同じです
    • vaddに対してvaddqの様に64bit/128bit幅でサイズが違うだけの命令の対が多数存在します。
  • 以上の規則、みたいなものを頭の片隅に入れて本アドベントカレンダーを読んで頂ければより理解が深まるのではないかと思います。

ロードとストアについて

  • vld1q_u8がメモリから読んでレジスタに格納するロード命令
  • vst1q_u8がレジスタからメモリに格納するストア命令
    uint8x16_t s0 = vld1q_u8(src0);
    uint8x16_t s1 = vld1q_u8(src1);
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
    vst1q_u8(dst, d);
  • loadstoreに関しては後日解説します
  • ベクトルの値から直接printするには、一度メモリに格納する必要があるため、そうしています。

おわりに

  • サンプルコードを示してSIMD演算を行いました
  • 明日も手島の担当で、add命令の解説から順番に始めます。
18
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?