如何防止GCC优化忙等待循环?

时间:2011-08-16 18:55:03

标签: c optimization gcc avr-gcc

我想为Atmel AVR微控制器编写C代码固件。我将使用GCC编译它。此外,我想启用编译器优化(-Os-O2),因为我认为没有理由不启用它们,并且它们可能比手动编写汇编更快地生成汇编方式。

但是我想要一小段没有优化的代码。我想延迟函数的执行一段时间,因此我想写一个do-nothing循环只是为了浪费一些时间。无需准确,只需等待一段时间。

/* How to NOT optimize this, while optimizing other code? */
unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i);
}

由于AVR中的内存访问速度慢很多,我希望ij保存在CPU寄存器中。


更新:我刚从util/delay.h找到了util/delay_basic.hAVR Libc。虽然大多数时候使用这些功能可能更好,但这个问题仍然有效且有趣。


相关问题:

9 个答案:

答案 0 :(得分:73)

我在跟踪dmckee's answer的链接后得出了这个答案,但它的方法与他/她的答案不同。

来自GCC的

Function Attributes文件提到:

  

noinline   此函数属性可防止将函数考虑为内联。如果函数没有副作用,那么除了内联之外,还有一些优化会导致函数调用被优化掉,尽管函数调用是实时的。为了防止此类调用被优化,请添加asm ("");

这给了我一个有趣的想法......我没有在内部循环中添加nop指令,而是尝试在其中添加一个空的汇编代码,如下所示:

unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i)
        asm("");
}

它有效!该循环尚未优化,并且未插入额外的nop指令。

更重要的是,如果你使用volatile,gcc会将这些变量存储在RAM中并添加一堆lddstd来将它们复制到临时寄存器。另一方面,这种方法不使用volatile并且不会产生这样的开销。


更新:如果您使用-ansi-std编译代码,则必须将asm关键字替换为__asm__,因为{{ 3}}

此外,如果您的described in GCC documentation

,也可以使用__asm__ __volatile__("")

答案 1 :(得分:21)

ij变量声明为volatile。这将阻止编译器优化涉及这些变量的代码。

unsigned volatile char i, j;

答案 2 :(得分:5)

我不确定为什么还没有提到这种方法完全被误导,并且很容易被编译器升级等打破。确定你想要等待和旋转的时间值会更有意义。轮询当前时间直到超过所需的值。在x86上,您可以使用rdtsc来实现此目的,但更便携的方式是调用clock_gettime(或非POSIX OS的变体)来获取时间。当前的x86_64 Linux甚至可以避免clock_gettime的系统调用,并在内部使用rdtsc。或者,如果您可以处理系统调用的成本,只需使用clock_nanosleep开始...

答案 3 :(得分:3)

如果编译器的avr版本支持full set of #pragmas(gcc版本4.4中所有日期链接中的有趣版本),我不知道我的头脑,但这是你通常的地方启动。

答案 4 :(得分:2)

对于我来说,在GCC 4.7.0上,无论如何使用-O3对空asm进行了优化(没有尝试使用-O2)。 并且在寄存器或volatile中使用i ++会导致性能损失(在我的情况下)。

我所做的是链接另一个空函数,编译器在编译“主程序”时无法看到

基本上这个:

使用此函数声明(空函数)

创建“helper.c”
void donotoptimize(){}

然后编译“gcc helper.c -c -o helper.o” 然后

while (...) { donotoptimize();}

这给了我最好的结果(据我所知,没有任何开销,但无法测试,因为没有它我的程序将无法工作:))

我认为它也适用于icc。如果启用链接优化,可能不会,但是使用gcc可以。

答案 5 :(得分:1)

将该循环放在单独的.c文件中,不要优化那个文件。甚至可以更好地在汇编程序中编写该例程并从C调用它,无论两种方式优化器都不参与。

我有时做易变的事情,但通常会创建一个简单返回的asm函数 调用该函数,优化器将使for / while循环变紧,但它不会优化它,因为它必须对虚函数进行所有调用。来自德尼尔森萨的回答做了同样的事情,但更加紧张......

答案 6 :(得分:1)

使用volatile asm应该会有所帮助。 你可以在这里阅读更多内容: -

http://www.nongnu.org/avr-libc/user-manual/optimization.html

如果您正在使用Windows,您甚至可以尝试将代码放在pragma下,详细说明如下: -

https://www.securecoding.cert.org/confluence/display/cplusplus/MSC06-CPP.+Be+aware+of+compiler+optimization+when+dealing+with+sensitive+data

希望这有帮助。

答案 7 :(得分:1)

空的__asm__语句还不够:请更好地使用数据依赖项

例如:

main.c

int main(void) {
    unsigned i;
    for (i = 0; i < 10; i++) {
        __asm__ volatile("");
    }
}

编译和反汇编:

gcc -O3 -ggdb3 -o main.out main.c
gdb -batch -ex 'disas main' main.out

输出:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     retq

还要注意,__asm__("")__asm__ volatile("")应该相同,因为没有输出操作数:The difference between asm, asm volatile and clobbering memory

如果我们将其替换为:

__asm__ volatile("");

产生:

   0x0000000000001040 <+0>:     nop
   0x0000000000001041 <+1>:     nop
   0x0000000000001042 <+2>:     nop
   0x0000000000001043 <+3>:     nop
   0x0000000000001044 <+4>:     nop
   0x0000000000001045 <+5>:     nop
   0x0000000000001046 <+6>:     nop
   0x0000000000001047 <+7>:     nop
   0x0000000000001048 <+8>:     nop
   0x0000000000001049 <+9>:     nop
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq

所以我们看到在这种情况下,GCC只是loop unrolled nop循环,因为该循环足够小。

因此,如果使用此技术,则将难以预测GCC二进制大小/速度的折衷,如果进行最佳应用,则应始终删除代码量为零的空__asm__ volatile("");的循环

我认为是可靠的一种解决方案,是将其替换为对循环变量i的显式数据依赖,如Enforcing statement order in C++

所建议
__asm__ ("" : "+g" (i) : :);

产生所需的循环:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     nopw   0x0(%rax,%rax,1)
   0x0000000000001048 <+8>:     add    $0x1,%eax
   0x000000000000104b <+11>:    cmp    $0x9,%eax
   0x000000000000104e <+14>:    jbe    0x1048 <main+8>
   0x0000000000001050 <+16>:    xor    %eax,%eax
   0x0000000000001052 <+18>:    retq 

这将i标记为嵌入式汇编的输入和输出。然后,内联汇编对于GCC来说是一个黑匣子,它不知道它如何修改i,因此我认为确实无法对其进行优化。

noinline忙循环功能

如果在编译时未知循环大小,则不可能完全展开,但是GCC仍可以决定分块展开,这会使您的延迟不一致。

将其与Denilson's answer放在一起,我会寻求:

void __attribute__ ((noinline)) busy_loop(unsigned max) {
    for (unsigned i = 0; i < max; i++) {
        __asm__ volatile("" : "+g" (i) : :);
    }
}

int main(void) {
    busy_loop(10);
}

在以下位置拆卸:

Dump of assembler code for function busy_loop:
   0x0000000000001140 <+0>:     test   %edi,%edi
   0x0000000000001142 <+2>:     je     0x1157 <busy_loop+23>
   0x0000000000001144 <+4>:     xor    %eax,%eax
   0x0000000000001146 <+6>:     nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000001150 <+16>:    add    $0x1,%eax
   0x0000000000001153 <+19>:    cmp    %eax,%edi
   0x0000000000001155 <+21>:    ja     0x1150 <busy_loop+16>
   0x0000000000001157 <+23>:    retq   
End of assembler dump.
Dump of assembler code for function main:
   0x0000000000001040 <+0>:     mov    $0xa,%edi
   0x0000000000001045 <+5>:     callq  0x1140 <busy_loop>
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq   
End of assembler dump.

这里需要volatile来将程序集标记为可能具有副作用,因为在这种情况下,我们有一个输出变量。

双循环版本可能是:

void __attribute__ ((noinline)) busy_loop(unsigned max, unsigned max2) {
    for (unsigned i = 0; i < max; i++) {
        for (unsigned j = 0; j < max2; j++) {
            __asm__ volatile ("" : "+g" (j), "+g" (j) : :);
        }
    }
}

int main(void) {
    busy_loop(10, 10);
}

GitHub upstream

相关线程:

在Ubuntu 19.04,GCC 8.3.0中进行了测试。

答案 8 :(得分:-1)

您也可以使用register keyword。用寄存器声明的变量存储在CPU寄存器中。

在你的情况下:

register unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i);
}