ARM 組み込みコンパイルのループ操作 (LOOP) 最適化

ループの反復回数によっては、ループが完了するまでに時間がかかる場合があり、また、反復のたびにループ条件が true であるかどうかを確認する必要があるため、ループのパフォーマンスも低下します。

目次

1 ループ展開 - ループ展開

 2 ループのベクトル化

 3 C言語でのループ終了

4 無限ループ


1 ループ展開 - ループ展開

ループごとに繰り返し条件を判断する必要があることによるパフォーマンスへの影響を軽減するために、ユーザーはループを拡張してループ条件を判断する回数を減らすことができます。 #pragma unroll (<n>) を使用して、ユーザー コードで時間とパフォーマンスに依存するループを展開します。ただし、ループの展開にはコード量が増加するという欠点もあります。以下の表の操作は、  、 、  、およびのみに適用されます。  -O2-O3-Ofast-Omax优化时有效果:

ループ展開プラグマ
プラグマ 説明
#pragma unroll (<n>) アンロールされたループの n 回の反復
#pragma unroll_completely ループ内のすべての反復を展開します

具体的な使用法については、次を参照してください:
#pragma unroll[(n)]、#pragma unroll_completely https://developer.arm.com/documentation/101754/0620/armclang-Reference/Compiler-specific-Pragmas/-pragma-unroll--n-----pragma-unroll-completely 注: 手動でループを展開し、ソース コードで #p を使用します ragma unroll (<n>) の効果は異なります。ソース コードでループをアンロールすると、コンパイラによるループの最適化が妨げられる可能性があるため、ARM では #pragma unroll (<n>) を使用することをお勧めします 。n が指定されていない場合、デフォルトではループ内のすべての反復が展開されます。さらに、コンパイラが反復回数を計算できない場合は、 #pragma unroll_completely を使用すると、コンパイル時にループは展開されません。

たとえば、次のサンプルコードは次のとおりです。

int countSetBits1(unsigned int n)
{
    int bits = 0;

    while (n != 0)
    {
        if (n & 1) bits++;
        n >>= 1;
    }
    return bits;
}

次のコマンドでコンパイルします。

armclang --target=arm-arm-none-eabi -march=armv8-a file.c -O2 -S -o file.s

 デフォルトでは、次のアセンブリ コードが取得されます。

countSetBits1:
        mov     r1, r0
        mov     r0, #0
        cmp     r1, #0
        bxeq    lr
        mov     r2, #0
        mov     r0, #0
.LBB0_1:
        and     r3, r1, #1
        cmp     r2, r1, asr #1
        add     r0, r0, r3
        lsr     r3, r1, #1
        mov     r1, r3
        bne     .LBB0_1
        bx      lr

ループ展開を4回行う場合: #pragma unroll (4)

int countSetBits2(unsigned int n)
{
    int bits = 0;
    #pragma unroll (4)
    while (n != 0)
    {
        if (n & 1) bits++;
        n >>= 1;
    }
    return bits;
}

生成されたアセンブリ コードは次のようになります。
 

countSetBits2:
        mov     r1, r0
        mov     r0, #0
        cmp     r1, #0
        bxeq    lr
        mov     r2, #0
        mov     r0, #0
LBB0_1:
        and     r3, r1, #1
        cmp     r2, r1, asr #1
        add     r0, r0, r3
        beq     .LBB0_4
@ BB#2:
        asr     r3, r1, #1
        cmp     r2, r1, asr #2
        and     r3, r3, #1
        add     r0, r0, r3
        asrne   r3, r1, #2
        andne   r3, r3, #1
        addne   r0, r0, r3
        cmpne   r2, r1, asr #3
        beq     .LBB0_4
@ BB#3:
        asr     r3, r1, #3
        cmp     r2, r1, asr #4
        and     r3, r3, #1
        add     r0, r0, r3
        asr     r3, r1, #4
        mov     r1, r3
        bne     .LBB0_1
.LBB0_4:
        bx      lr

反復回数がコンパイル時に決定できる場合、ARM 組み込みコンパイラはループを完全に展開できます。

 2 ループのベクトル化

ユーザー コードのオブジェクトに Advanced SIMD ユニットが含まれている場合、ARM 組み込みコンパイラはベクトル化エンジンを使用して、ベクトル化できるコードの部分を最適化できます。最適化レベルでは、 -O1上,可以使用 ベクトル化を有効にする -fvectorize オプション。-O1 より高い最適化レベルでは、 -fvectorize がデフォルトで有効になり、ユーザーは  -fno-vectorize 选项将其关闭。详情见:-fvectorize 、 -fno-vectorizeを使用できます 。

Advanced SIMD を使用してベクトル化されたコード例:

typedef struct tBuffer {
  int a;
  int b;
  int c;
} tBuffer;
tBuffer buffer[8];

void DoubleBuffer1 (void)
{
  int i;
  for (i=0; i<8; i++)
  {
    buffer[i].a *= 2;
    buffer[i].b *= 2;
    buffer[i].c *= 2;
  }
}

最適化レベル -O2 でコンパイルします。

armclang --target=arm-arm-none-eabi -march=armv8-a -O2 file.c -S -o file.s

次のコードが取得されます。
 

DoubleBuffer1:
.fnstart
@ BB#0:
        movw    r0, :lower16:buffer
        movt    r0, :upper16:buffer
        vld1.64 {d16, d17}, [r0:128]
        mov     r1, r0
        vshl.i32        q8, q8, #1
        vst1.32 {d16, d17}, [r1:128]!
        vld1.64 {d16, d17}, [r1:128]
        vshl.i32        q8, q8, #1
        vst1.64 {d16, d17}, [r1:128]
        add     r1, r0, #32
        vld1.64 {d16, d17}, [r1:128]
        vshl.i32        q8, q8, #1
        vst1.64 {d16, d17}, [r1:128]
        add     r1, r0, #48
        vld1.64 {d16, d17}, [r1:128]
        vshl.i32        q8, q8, #1
        vst1.64 {d16, d17}, [r1:128]
        add     r1, r0, #64
        add     r0, r0, #80
        vld1.64 {d16, d17}, [r1:128]
        vshl.i32        q8, q8, #1
        vst1.64 {d16, d17}, [r1:128]
        vld1.64 {d16, d17}, [r0:128]
        vshl.i32        q8, q8, #1
        vst1.64 {d16, d17}, [r0:128]
        bxlr

SIMD を使用しない場合:

typedef struct tBuffer {
  int a;
  int b;
  int c;
} tBuffer;
tBuffer buffer[8];

void DoubleBuffer2 (void)
{
  int i;
  for (i=0; i<8; i++)
    buffer[i].a *= 2;
  for (i=0; i<8; i++)
    buffer[i].b *= 2;
  for (i=0; i<8; i++)
    buffer[i].c *= 2;
}

取得します:

DoubleBuffer2:
.fnstart
@ BB#0:
        movw    r0, :lower16:buffer
        movt    r0, :upper16:buffer
        ldr     r1, [r0]
        lsl     r1, r1, #1
        str     r1, [r0]
        ldr     r1, [r0, #12]
        lsl     r1, r1, #1
        str     r1, [r0, #12]
        ldr     r1, [r0, #24]
        lsl     r1, r1, #1
        str     r1, [r0, #24]
        ldr     r1, [r0, #36]
        lsl     r1, r1, #1
        str     r1, [r0, #36]
        ldr     r1, [r0, #48]
        lsl     r1, r1, #1
        str     r1, [r0, #48]
        ldr     r1, [r0, #60]
        lsl     r1, r1, #1
        str     r1, [r0, #60]
        ldr     r1, [r0, #72]
        lsl     r1, r1, #1
        str     r1, [r0, #72]
        ldr     r1, [r0, #84]
        lsl     r1, r1, #1
        str     r1, [r0, #84]
        ldr     r1, [r0, #4]
        lsl     r1, r1, #1
        str     r1, [r0, #4]
        ldr     r1, [r0, #16]
        lsl     r1, r1, #1
        ...
        bx      lr

Neon テクノロジである Advanced SIMD (Single Instruction Multiple Data) は、ARMv7-A シリーズ以降のアーキテクチャで使用されています。ユーザーは、より高性能に最適化されたコードを作成できます。Neon の使用に関して、ユーザーは、C/C++ 関数インターフェイスを直接使用して呼び出すことができます。Neon の使用スキルについては、記事を参照してください: Arm C 言語拡張機能 ACLE Q1
2019

 Cortex-A シリーズ プログラマーズ ガイド

Arm Neon プログラマーズ ガイド

 使用 -fno-vectorize 选项并不能完全阻止编译器忽略SIMD指令。如果链接库包含了Neon相关指令,编译器或者链接器仍或使用SIMD。

コンパイラーが AArch64 ターゲットに対して高度な SIMD 命令を発行しないようにするには、-march または -mcpu とともに +nosimd を指定します。

armclang --target=aarch64-arm-none-eabi -march=armv8-a+nosimd -O2 file.c -S -o file.s

コンパイラーが AArch32 ターゲットに対してアドバンスト SIMD 命令を発行しないようにするには、オプション -mfpu をアドバンスト SIMD を含まない正しい値に設定します。たとえば、-mfpu=fp-armv8 となります。

armclang --target=aarch32-arm-none-eabi -march=armv8-a -mfpu=fp-armv8 -O2 file.c -S -o file.s

 3 C言語でのループ終了

コードが注意深く書かれていない場合、ループ終了条件により重大なパフォーマンスのオーバーヘッドが発生する可能性があります。たとえば、次のような状況があります。

  • 単純なループ終了条件を使用する
  • 0 までデクリメントし、0 に等しいかどうかをチェックするループを作成します。
  • 符号なし整数型を使用したカウンター: unsigned int

たとえば、階乗数 n! は 2 つあります。関数:
 

int fact1(int n)
{
    int i, fact = 1;
    for (i = 1; i <= n; i++)
        fact *= i;
    return (fact);
}

int fact2(int n)
{
    unsigned int i, fact = 1;
    for (i = n; i != 0; i--)
        fact *= i;
    return (fact);
}

ファクト 1 はインクリメントを使用し、ファクト 2 はデクリメントを使用します。

if コマンドでコンパイルします。

armclang -Os -S --target=arm-arm-none-eabi -march=armv8-a

 結果として得られるアセンブリは次のとおりです。

インクリメント階乗関数 fat1:

; r1 -> n
; r0 -> fact
; r2 -> i


fact1:
        mov     r1, r0
        mov     r0, #1
        cmp     r1, #1
        bxlt    lr
        mov     r2, #0
.LBB0_1:
        add     r2, r2, #1
        mul     r0, r0, r2
        cmp     r1, r2
        bne     .LBB0_1
        bx      lr

減少階乗関数の事実 2:

; r1 -> i
; r0 -> fact

fact2:
        mov     r1, r0
        mov     r0, #1
        cmp     r1, #0
        bxeq    lr
.LBB1_1:
        mul     r0, r0, r1
        subs    r1, r1, #1
        bne     .LBB1_1
        bx      lr

増加関数と減少関数を比較すると、次のことがわかります。

  1. fat1 関数は、fact2 関数よりも 1 つ多い CMP r1、r2命令を使用します。fact1 は、最初に ADD 命令を使用して自己インクリメント演算を実行し、次に CMP 命令を使用して i と n のサイズを比較します。ただし、fact2 関数には 1 つデクリメントする SUBS 命令のみが必要です。これは、SUBS 演算によって CPSR の Z フラグが更新されるためであり、CMP 命令を使用せずに条件付きジャンプを実現できます。
  2. fat1 のもう 1 つの欠点は、fact2 よりもレジスタ R2 を 1 つ多く使用することです。これは、fact1 関数の i を 1 を加算した後に n と比較する必要があるため、n の値を保存するために追加のレジスタが必要になるためです。n はループの存続期間中は必須ではありません。維持するレジスタが少ないほど、プログラムによるレジスタの割り当てが容易になります。

要約すると、減分ループを使用する方が簡単で効率的です。

元の終了条件に関数呼び出しが含まれていた場合、その関数が返す値が同じであっても、ループの反復ごとにその関数が呼び出される可能性があります。この場合、減分ループを使用するのもより効率的です。例えば:

for (...; i < get_limit(); ...);

ループ カウンター (i) を必要な反復回数 (n) に初期化し、それを 0 にデクリメントする方法は、while ステートメントと do ステートメントでも機能します。

4 無限ループ

 C11 および C++11 標準で規定されているように、armclang は、副作用のない無限ループを未定義の動作とみなします。場合によっては、armclang は副作用のない無限ループを削除または移動するため、プログラムが終了したり、期待どおりに動作しなくなる可能性があります。

ループを無限の時間実行できるようにするために、Arm では__asm volatileステートメントを含む無限ループを作成することをお勧めします。volatile キーワードは、ループを潜在的な副作用として考慮するようにコンパイラーに指示し、ループを削除する最適化を防ぎます。このようなループでは、イベントまたは割り込みが発生するまでプロセッサを低電力状態にすることも推奨されます。次の例は、イベントが発生するまでプロセッサを低電力状態にする命令 WFE を含む、揮発性として指定された無限ループを示しています。

void infinite_loop(void) {
while (1)
  __asm volatile("wfe");
}

volatile キーワードは、armclang にループを削除または移動しないよう指示します。コンパイラはループに副作用があると見なすため、最適化中にループを削除しません。イベント待機アセンブリ命令はプロセッサにヒントを与えます。この方法でループを作成すると、WFE 命令を実装するプロセッサは、イベントまたは割り込みが発生するまで低電力状態に入ることができるため、ループが不必要に電力を消費することがなくなります。WFI (割り込み待ち) を使用して、WFI (割り込み待ち) を実装したプロセッサの実行を可能にする WFI 命令を含むコードを出力することもできます。

参考記事:

ループの最適化https://developer.arm.com/documentation/100748/0620/Writing-Optimized-Code/Optimizing-loops?lang=ja

おすすめ

転載: blog.csdn.net/luolaihua2018/article/details/130714061