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

Den*_*aia 62 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);
}
Run Code Online (Sandbox Code Playgroud)

由于AVR中的内存访问速度要慢得多,因此我希望i并将j其保存在CPU寄存器中.


更新:我刚刚发现UTIL/delay.hUTIL/delay_basic.hAVR libc库.尽管大多数情况下使用这些功能可能更好,但这个问题仍然有效且有趣.


相关问题:

Den*_*aia 77

我根据dmckee的回答链接后得出了这个答案,但它采用了与他/她的答案不同的方法.

GCC的功能属性文档提到:

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

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

unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i)
        asm("");
}
Run Code Online (Sandbox Code Playgroud)

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

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


更新:如果您使用-ansi或编译代码-std,则必须将asm关键字替换为__asm__,如GCC文档中所述.

此外,__asm__ __volatile__("")如果你的汇编语句必须在我们放置它的地方执行,你也可以使用(即不得将其作为优化移出循环).

  • 您将如何在Visual Studio中执行此操作?`__asm {“”}`不起作用 (2认同)

ks1*_*322 21

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

unsigned volatile char i, j;
Run Code Online (Sandbox Code Playgroud)

  • 虽然这有效,但它有将这些变量强制存储的副作用.因此,GCC将在每次循环迭代中读取和写入它们,从而增加了相当多的开销.(无论如何,如果我想要非常细粒度的控制,我应该直接编写汇编) (7认同)
  • @DenilsonSá 另一方面,强制内存访问将确保等待总是花费相同的时间,无论该值是否为 16 位可编码,都是独立的。 (2认同)

Cir*_*四事件 10

__asm__语句是不够的:更好地使用数据依赖

像这样:

主文件

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

    }
}
Run Code Online (Sandbox Code Playgroud)

编译和反汇编:

gcc -O3 -ggdb3 -o main.out main.c
gdb -batch -ex 'disas main' main.out
Run Code Online (Sandbox Code Playgroud)

输出:

   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 
Run Code Online (Sandbox Code Playgroud)

我相信这是健壮的,因为它在循环变量上放置了显式数据依赖项i在 C++ 中强制执行语句顺序并生成所需的循环:

这标记i为内联汇编的输入和输出。那么,内联汇编是 GCC 的一个黑盒子,它无法知道它是如何修改 的i,所以我认为这真的无法优化掉。

如果我对空做同样的事情__asm__

坏.c

int main(void) {
    unsigned i;
    for (i = 0; i < 10; i++) {
        __asm__ volatile("");
    }
}
Run Code Online (Sandbox Code Playgroud)

它似乎完全删除了循环和输出:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     retq
Run Code Online (Sandbox Code Playgroud)

另请注意,__asm__("")__asm__ volatile("")应该相同,因为没有输出操作数:asm、asm volatile 和 clobbering memory 之间的区别

如果我们将其替换为:

__asm__ volatile("nop");
Run Code Online (Sandbox Code Playgroud)

它产生:

   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
Run Code Online (Sandbox Code Playgroud)

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

因此,如果您依赖 empty __asm__,您将依赖于难以预测的 GCC 二进制大小/速度权衡,如果以最佳方式应用,应该始终删除__asm__ volatile("");代码大小为零的 empty 的循环。

noinline 忙循环功能

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

将其与Denilson 的答案放在一起,可以将繁忙循环函数写为:

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

int main(void) {
    busy_loop(10);
}
Run Code Online (Sandbox Code Playgroud)

在以下位置拆卸:

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.
Run Code Online (Sandbox Code Playgroud)

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

双循环版本可能是:

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

int main(void) {
    busy_loop(10, 10);
}
Run Code Online (Sandbox Code Playgroud)

GitHub 上游.

相关主题:

在 Ubuntu 19.04、GCC 8.3.0 中测试。


R..*_*R.. 5

我不确定为什么尚未提及该方法,因为该方法完全被误导,容易被编译器升级等破坏。确定要等待直到开始旋转轮询时间的时间值会更有意义。直到超过所需值的时间。在x86上,您可以使用rdtsc此方法,但是更可移植的方法是调用clock_gettime(或非POSIX OS的变体)以获取时间。当前的x86_64 Linux甚至会避免在内部clock_gettime使用syscall rdtsc。或者,如果您可以负担syscall的费用,则只需使用clock_nanosleep...