11

目标

我目前正在尝试 avr-llvm(支持 AVR 作为目标的 llvm)。我的主要目标是使用它希望更好的优化器(与 gcc 相比)来实现更小的二进制文件。如果您对 AVR 稍有了解,您就会知道您只有很少的内存。

我目前使用 ATtiny45、4KB 闪存和 256 字节(只是字节而不是 KB!)的 SRAM。

问题

我正在尝试编译一个简单的 C 程序(见下文),以检查生成了哪些汇编代码以及机器代码大小是如何发展的。我使用“clang -Oz -S test.c”来生成汇编输出并优化它以实现最小尺寸。我的问题是不必要地保存了寄存器值,因为我知道这种方法永远不会返回。

我的问题...

如果需要,我如何告诉 llvm 它可以破坏任何寄存器而不保存/恢复它的内容?任何想法如何进一步优化它(例如更有效的堆栈设置)?

详细信息/示例

这是我的测试程序。如上所述,它是使用“clang -Oz -S test.c”编译的。

#include <stdint.h>

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

如您所见,它只有一个 uint8_t 类型的“易失性”变量(如果我不将其设置为易失性,一切都会被优化)。这个变量设置为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 程序要做的事情。它使用值“1”加载 r24,该值存储在 Y+1(堆栈指针 + 1)处的内存中。当然还有我们的无限循环:

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”高字节大头钉“P”指针)
  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 和闪存浪费在愚蠢的指令上(请记住:我们在某些设备中只有 64、128 或 256 字节的 SRAM)。

我对此进行了进一步调查:如果我们让 main 返回(例如没有无限循环)堆栈指针被恢复,我们在末尾有一个“ret”指令并且寄存器 r28 和 r29 通过“pop r29,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 version '3.2 (trunk 160228) (based on LLVM 3.2svn)' 既不知道 OS_task 也不知道 OS_main)。

4

1 回答 1

3

Anton 在他的评论中提出了这个问题的答案:问题不在于 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

正如您所看到的所有这三个目标,生成的唯一代码是保留堆栈空间(如果需要,它不在 x86-64 上)并在堆栈上设置值。我认为这是最小的。

也就是说,如果您确实发现 LLVM 的优化器有问题,获得帮助的最佳方式是向开发邮件列表发送电子邮件,或者如果您有一个应该产生更小的输出 IR 的特定输入 IR 序列,则提交错误。

最后,回答您在评论中提出的问题:实际上,LLVM 的优化器在某些领域比 GCC 强大得多。但是,也有一些领域它的功能明显不那么强大。=] 对您关心的代码进行基准测试。

于 2013-07-07T07:39:53.647 回答