11

目標

私は現在avr-llvm(ターゲットとしてAVRをサポートするllvm)を試しています。私の主な目標は、(gccのものと比較して)より優れたオプティマイザーを使用して、より小さなバイナリーを実現することです。AVRについて少し知っている場合は、メモリが少ないことを知っています。

私は現在、ATTiny45、4KBフラッシュ、および256バイト(KBではなくバイトだけです!)のSRAMを使用しています。

問題

単純なCプログラム(以下を参照)をコンパイルして、生成されるアセンブリコードと、マシンコードのサイズがどのように変化しているかを確認しようとしていました。「clang-Oz-Stest.c」を使用して、アセンブリ出力を生成し、最小サイズに最適化しました。私の問題は、このメソッドが決して返されないことを知っている、不必要に保存されたレジスタ値です。

私の質問...

必要に応じて、コンテンツを保存/復元せずに、任意のレジスタを壊すことができることをllvmに伝えるにはどうすればよいですか?それをさらに最適化する方法(スタックのより効率的なセットアップなど)はありますか?

詳細/例

これが私のテストプログラムです。上記のように、「clang-Oz-Stest.c」を使用してコンパイルされました。

#include <stdint.h>

void __attribute__ ((noreturn)) main()  {
     volatile uint8_t res = 1;
     while (1) {}
}

ご覧のとおり、uint8_t型の「揮発性」変数が1つだけあります(揮発性に設定しないと、すべてが最適化されます)。この変数は1に設定されています。最後に無限ループがあります。次に、アセンブリの出力を見てみましょう。

.file   "test.c"
    .text
    .globl  main
    .align  2
    .type   main,@function
main:
    push    r28
    push    r29
    in  r28, 61
    in  r29, 62
    sbiw    r29:r28, 1
    in  r0, 63
    cli
    out 62, r29
    out 63, r0
    out 61, r28
    ldi r24, 1
    std Y+1, r24
.BB0_1:
    rjmp    .BB0_1
.tmp0:
    .size   main, .tmp0-main

うん!これは、このような単純なプログラムの多くのマシンコードです。私はちょうどいくつかのバリエーションをテストし、AVRのリファレンスマニュアルを調べました...それで私は何が起こるかを説明することができます。それぞれの部分を見てみましょう。

これが「牛肉」です。これは、cプログラムの目的を実行しているだけです。Y + 1(スタックポインタ+1)でメモリに格納されている値「1」でr24をロードします。そしてもちろん、私たちの無限のループがあります:

ldi r24, 1
std Y+1, r24

.BB0_1:
    rjmp    .BB0_1

注:エンドレスループが必要であることに注意してください。それ以外の場合__attribute__ ((noreturn))は無視され、スタックポインタ+保存されたレジスタは後で復元されます。

その直前に、「Y」のポインタが設定されます。

in  r28, 61
in  r29, 62
sbiw    r29:r28, 1
in  r0, 63
cli
out 62, r29
out 63, r0
out 61, r28

ここで何が起こるかです:

  1. Y(レジスタペアr28:r29は「Y」に相当)はポート61および62からロードされ、これらのポートはいくつかの「レジスタ」、つまりSPLおよびSPH(「S」の「L」owおよび「H」ighバイト)にマップされます。タック「P」ointer)
  2. ロードされた値がデクリメントされます(sbiw r29:r28)
  3. スタックポインタの変更された値は、ポートに保存されます。そして私は問題を避けるために推測します:割り込みは以前に無効にされています。「cli/sti」[レジスタ63(SREG)に格納されている]の状態はr0に保存され、後でポート63に復元されます。

このスタックレジスタの設定は非効率的なようです。スタックポインタをインクリメントするには、スタックに「r0」をプッシュする必要があります。次に、SPH / SPLの値をr29:r28にロードするだけです。ただし、これには、ソースコードのllvmのオプティマイザにいくつかの変更が必要になる可能性があります。上記のコードは、3バイトを超えるスタックをローカル変数用に予約する必要がある場合にのみ意味があります(-O3を最適化する場合でも、-Ozの場合は最大6バイトで意味があります)。ただし、そのためにはllvmのソースに触れる必要があると思います。したがって、これは範囲外です。

さらに興味深いのは、この部分です。

    push    r28
    push    r29

main()は戻ることを意図していないため、これは意味がありません。これは、ばかげた命令のためにRAMとフラッシュメモリを浪費するだけです(一部のデバイスで使用できるSRAMは64、128、または256バイトしかないことに注意してください)。

これをもう少し詳しく調べました。メインリターン(無限ループがないなど)をスタックポインタに戻すと、最後に「ret」命令があり、レジスタr28とr29が「popr29、pop」を介してスタックから復元されます。 28"。ただし、コンパイラは、関数 "main"のスコープが決して残されていない場合、スタックに格納せずにすべてのレジスタを壊してしまう可能性があることを知っておく必要があります。

2バイトのRAMについて話すと、この問題は少し「ばかげている」ように見えます。しかし、プログラムが残りのレジスタの使用を開始した場合にどうなるかを考えてみてください。

これらすべてが、現在の「コンパイラ」に対する私の見方を本当に変えました。今日は、アセンブラーによる最適化の余地はあまりないと思いました。しかし、あるようです...

それで、それでも問題は...

この状況を改善する方法を知っていますか(バグレポート/機能リクエストの提出を除く)?

つまり、見落としている可能性のあるコンパイラスイッチがいくつかあるだけですか...?

追加情報

使用__attribute__ ((OS_main))はavr-gccで動作します。

出力は次のとおりです。

    .file   "test.c"
__SREG__ = 0x3f
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__CCP__  = 0x34
__tmp_reg__ = 0
__zero_reg__ = 1
    .global __do_copy_data
    .global __do_clear_bss
    .text
.global main
    .type   main, @function
main:
    push __tmp_reg__
    in r28,__SP_L__
    in r29,__SP_H__
/* prologue: function */
/* frame size = 1 */
    ldi r24,lo8(1)
    std Y+1,r24
.L2:
    rjmp .L2
    .size   main, .-main

これは(私の意見では)サイズ(6命令または12バイト)で最適であり、このサンプルプログラムの速度でも最適です。llvmに相当する属性はありますか?(clangバージョン '3.2(trunk 160228)(LLVM 3.2svnに基づく)'はOS_taskについても、OS_mainについても何も知りません)。

4

1 に答える 1

3

尋ねられた質問への答えは、アントンが彼のコメントでいくらか提起しています。問題はLLVMにあるのではなく、AVRターゲットにあります。たとえば、他のターゲットに対してClangとLLVMを介して実行される同等のプログラムは次のとおりです。

% cat test.c
__attribute__((noreturn)) int main() {
  volatile unsigned char res = 1;
  while (1) {}
}

% ./bin/clang -c -o - -S -Oz test.c  # I'm on an x86-64 machine
<snip>
main:                                   # @main
        .cfi_startproc
# BB#0:                                 # %entry
        movb    $1, -1(%rsp)
.LBB0_1:                                # %while.body
                                        # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Ltmp0:
        .size   main, .Ltmp0-main
        .cfi_endproc

% ./bin/clang -c -o - --target=armv6-unknown-linux-gnueabi -S -Oz test.c
<snip>
main:
        sub     sp, sp, #4
        mov     r0, #1
        strb    r0, [sp, #3]
.LBB0_1:
        b       .LBB0_1
.Ltmp0:
        .size   main, .Ltmp0-main

% ./bin/clang -c -o - --target=powerpc64-unknown-linux-gnu -S -Oz test.c
<snip>
main:
        .align  3
        .quad   .L.main
        .quad   .TOC.@tocbase
        .quad   0
        .text
.L.main:
        li 3, 1
        stb 3, -9(1)
.LBB0_1:
        b .LBB0_1
        .long   0
        .quad   0
.Ltmp0:
        .size   main, .Ltmp0-.L.main

これら3つのターゲットすべてでわかるように、生成されるコードは、スタックスペースを予約し(必要に応じて、x86-64にはありません)、スタックに値を設定することだけです。これは最小限だと思います。

とはいえ、LLVMのオプティマイザーに問題が見つかった場合、ヘルプを得る最善の方法は、開発メーリングリストに電子メールを送信するか、より最小限の出力IRを生成する特定の入力IRシーケンスがある場合はバグを報告することです。

最後に、あなたの質問に対するコメントで尋ねられた質問に答えるために:LLVMのオプティマイザーがGCCよりもはるかに強力な領域が実際にあります。ただし、それが大幅に弱い領域もあります。=]気になるコードをベンチマークします。

于 2013-07-07T07:39:53.647 に答える