組み込みの埋まってるとこ

システム寄りの組み込みCプログラミングBLOG

ベクタライズされる基本的なプログラム例

Cコンパイラの最適化でベクタライズされるプログラムの基本的な条件を実験しながら確認してみました。

ベクタライズされるプログラムとされないプログラム

演算に使用される変数をそれぞれ配列、構造体、スカラーとしたとき、 ベクタライズされるかどうかを確認してみます。

今回の確認にはARM Linux用のGCC 12を使用しています。 特に断りがなければコンパイルオプションは次のとおりです。

-O3 -ftree-vectorize -march=armv7-a+simd

変数が配列になっているときはベクタライズ可能

変数を配列にすることはベクタライズを意識したプログラムの基本ですので まずそのとおりやってみます。

void add_array(
  const int32_t *restrict a,
  const int32_t *restrict b,
  int32_t *restrict c)
{
    c[0] = a[0] + b[0];
    c[1] = a[1] + b[1];
}

結果をアセンブラで確認すると期待どおり綺麗にベクタライズされています。 aとbそれぞれで32ビットの値が2つ同時に64ビットのdレジスタにロードされ、 その後の加算と結果のストアもずっと2つ同時になっています。

add_array:
        vld1.32 {d16}, [r0]
        vld1.32 {d17}, [r1]
        vadd.i32        d16, d16, d17
        vst1.32 {d16}, [r2]
        bx      lr

変数が構造体になっているときベクタライズされる条件

構造体の中の変数どうしの演算がベクタライズできるかどうかはやや不確かになります。 理論的には変数が配列にしたときのように連続したアドレスに配置されればできるはずです。 例えば次のようなシンプルな構造体であればまず問題ないでしょう。

struct vec2 {
 int32_t x;
 int32_t y;
 };

void add_struct(
  const struct vec2 *restrict a,
  const struct vec2 *restrict b,
  struct vec2 *restrict c)
{
    c->x = a->x + b->x;
    c->y = a->y + b->y;
}

結果は先ほどの変数が配列のときと同じになりました。

add_struct:
        vld1.32 {d16}, [r0]
        vld1.32 {d17}, [r1]
        vadd.i32        d16, d16, d17
        vst1.32 {d16}, [r2]
        bx      lr

変数がスカラーのときはベクタライズされない

変数がスカラーのときにベクタライズされることはおそらくないでしょう。 ベクタライズされない原因として単純にコンパイラがベクタライズ可能であると認識するのが難しいということもあるのでしょうが、 もし仮に認識できたとしても スカラーとしてプログラムされている変数が連続したアドレスに割り当てられることは保障できず、 個別にロードして1つのレジスタに詰め込んでから演算したのではコストが高くなり過ぎます。

void add_scalar(
  int32_t a_x,
  int32_t a_y,
  int32_t b_x,
  int32_t b_y,
  int32_t *c_x,
  int32_t *c_y)
{
    *c_x = a_x + b_x;
    *c_y = a_y + b_y;
}

ベクタライズされていないので加算命令addも2回でてきています。

add_scalar:
        add     r1, r1, r3
        ldr     r3, [sp]
        add     r0, r0, r2
        str     r0, [r3]
        ldr     r3, [sp, #4]
        str     r1, [r3]
        bx      lr

ポインタはrestrictで型修飾されていないとベクタライズされないことがある

ベクタライズのプログラム例には restrictというあまり見かけない型修飾がついていることがよくあるのですが これは必須なのでしょうか?

restrictはポインタが指しているデータ領域が他のポインタから参照されているデータ領域と重複していないという意味になります。 ではベクタライズするときデータ領域が重複していると何が問題かというと、 出力と入力のデータ領域が一部重複しているときは出力したデータで入力のデータ領域を破壊することが理論的に起こり得るため、 コンパイラはベクタライズを躊躇するようになってしまいます。

下記の関数を例にすると、入力のaと入力のbはrestrictである必要はなく同じデータ領域であったとしても問題ないのですが、 出力のcと入力のa、または出力のcと入力のbが重複したデータ領域である場合には問題になります。 そのためcがrestrictであるか、またはaとbが共にrestrictである(aとbはどちらもcと重複していない)ことがベクタライズされるための条件になります。

void add_array_non_restrict(
  const int32_t *a,
  const int32_t *b,
  int32_t *c)
{
    c[0] = a[0] + b[0];
    c[1] = a[1] + b[1];
}
add_array_non_restrict:
        ldr     r3, [r0]
        push    {r4}
        ldr     r4, [r1]
        add     r3, r3, r4
        str     r3, [r2]
        ldr     r4, [sp], #4
        ldr     r3, [r0, #4]
        ldr     r1, [r1, #4]
        add     r3, r3, r1
        str     r3, [r2, #4]
        bx      lr

上記のプログラムを少し手直して、ポインタとしてではなく配列の実体を参照するようにするとrestrictが無くてもデータ領域が重複しないことが明らかなのでベクタライズされます。

void add_array_ref()
{
    extern int32_t a[2];
    extern int32_t b[2];
    extern int32_t c[2];

    c[0] = a[0] + b[0];
    c[1] = a[1] + b[1];
}
add_array_ref:
        movw    r1, #:lower16:a
        movt    r1, #:upper16:a
        movw    r2, #:lower16:b
        movt    r2, #:upper16:b
        movw    r3, #:lower16:c
        movt    r3, #:upper16:c
        vld1.32 {d16}, [r1]
        vld1.32 {d17}, [r2]
        vadd.i32        d16, d16, d17
        vst1.32 {d16}, [r3]
        bx      lr

ループ回数が変数になっているときのベクタライズ

ループ回数が変数になっているプログラムをベクタライズするのは技術的に難しいと思うのですが、 これは圧巻の力技で解決されます。

例えば次のプログラムは不定回の代入を行うプログラムですが、 GCC 12ではループの残り回数が4以上のときは4並列で代入し、それ以外の残り回数のときは1つずつ代入するという それなりにボリュームのあるコードにベクタライズされました。 ここのところがどのようにベクタライズされるかはGCCのバージョンによりかなり異なるようです。

void variable_loop(
  int32_t n,
  int32_t *restrict a,
  int num)
{
  for (int i = 0; i < num; i++) {
    a[i] = n;
  }
}
variable_loop:
        cmp     r2, #0
        ble     .L9
        subs    r3, r2, #1
        cmp     r3, #2
        bls     .L6
        lsr     ip, r2, #2
        vdup.32 q8, r0
        mov     r3, r1
        add     ip, r1, ip, lsl #4
.L4:
        vst1.32 {q8}, [r3]!
        cmp     ip, r3
        bne     .L4
        bic     r3, r2, #3
        cmp     r2, r3
        beq     .L12
.L3:
        add     ip, r3, #1
        push    {lr}
        cmp     r2, ip
        lsl     lr, r3, #2
        str     r0, [r1, r3, lsl #2]
        ble     .L1
        add     r1, r1, lr
        adds    r3, r3, #2
        cmp     r2, r3
        str     r0, [r1, #4]
        it      gt
        strgt   r0, [r1, #8]
.L1:
        ldr     pc, [sp], #4
.L9:
        bx      lr
.L12:
        bx      lr
.L6:
        movs    r3, #0
        b       .L3