如何告诉clang不要将寄存器保存到堆栈中?

时间:2012-10-11 23:31:39

标签: llvm clang avr llvm-clang optimization

目标

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

我目前使用的是一个ATTiny45,4KB Flash和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的“volatile”变量(如果我没有将它设置为volatile,那么所有内容都会被优化掉)。此变量设置为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程序。它加载r24的值为“1”,它存储在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(“L”ow和“H”高字节的“S”大头钉“P”ointer)
  2. 加载的值递减(sbiw r29:r28)
  3. 将堆栈指针的更改值保存回端口;我想避免出现问题:中断被禁用; “cli / sti”[存储在寄存器63(SREG)]中的状态保存到r0,然后恢复到端口63.
  4. 堆栈寄存器的这种设置似乎效率低下。要增加堆栈指针,我只需要“将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版本'3.2(主干160228)(基于LLVM 3.2svn)'既不知道OS_task也不了解OS_main的任何信息。)

1 个答案:

答案 0 :(得分: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

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

也就是说,如果你确实发现了LLVM优化器的问题,获得帮助的最好方法是发送电子邮件到开发邮件列表或者如果你有一个特定的输入IR序列应该产生更多的最小输出IR。

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