目標
私は現在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
ここで何が起こるかです:
- Y(レジスタペアr28:r29は「Y」に相当)はポート61および62からロードされ、これらのポートはいくつかの「レジスタ」、つまりSPLおよびSPH(「S」の「L」owおよび「H」ighバイト)にマップされます。タック「P」ointer)
- ロードされた値がデクリメントされます(sbiw r29:r28)
- スタックポインタの変更された値は、ポートに保存されます。そして私は問題を避けるために推測します:割り込みは以前に無効にされています。「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についても何も知りません)。