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

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

但我想一个小块的代码没有优化。我希望将函数的执行延迟一些时间,因此我想编写一个不执行任何操作的循环来浪费一些时间。不需要精确,只需要等待一段时间。

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

Since memory access in AVR is a lot slower, I want i and j to be kept in CPU registers.


更新: 我刚从 AVR Libc中找到了 util/delay.hUtil/late _ basic. h。尽管在大多数情况下,使用这些函数可能是一个更好的主意,但是这个问题仍然是有效和有趣的。


相关问题:

70279 次浏览

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

unsigned volatile char i, j;

我不知道编译器的 avr 版本是否支持 全套 #pragma(链接中有趣的部分都来自 gcc 版本4.4) ,但这是您通常会开始的地方。

我根据 Dmckee 的回答中的一个链接得出了这个答案,但是它采用了一种与他/她的答案不同的方法。

函数属性 来自 GCC 的文档提到:

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__,即 在海湾合作委员会文件中描述

此外,如果您的 装配语句必须在我们放置它的地方执行(即不能作为优化从循环中移出)

把那个循环放在一个单独的。文件,不要优化那个文件。更好的做法是在汇编程序中编写这个例程,并从 C 调用它,无论哪种方式,优化器都不会参与进来。

我有时会做一些不稳定的操作但通常会创建一个简单返回的 asm 函数 调用这个函数优化器会使 for/while 循环紧凑,但不会优化它,因为它必须调用所有的虚函数。Denilson Sá 的否定回答也是一样,但是更加严格..。

用不稳定性高潮应该会有帮助。 你可以在这里阅读更多:-

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

如果您正在使用 Windows,您甚至可以尝试将代码置于杂注之下,详细说明如下:-

Https://www.securecoding.cert.org/confluence/display/cplusplus/msc06-cpp.+be+aware+of+compiler+optimization+when+dealing+with+sensitive+data

希望这个能帮上忙。

我不知道为什么还没有提到这种方法是完全错误的,很容易被编译器升级等打破。确定您希望等待的时间值,并旋转轮询当前时间,直到超出所需的值,这样做更有意义。在 x86上,可以为此使用 rdtsc,但是更可移植的方法是调用 clock_gettime(或非 POSIX OS 的变体)来获取时间。当前的 x86 _ 64 Linux 甚至可以避免对 clock_gettime的系统调用,并在内部使用 rdtsc。或者,如果您可以处理一个系统调用的成本,只需使用 clock_nanosleep开始..。

也可以使用 关键字 < strong > register 关键字.register 声明的变量存储在 CPU 寄存器中。

In your case:

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

对于我来说,在 GCC 4.7.0上,空高潮是用 -O3优化的(没有尝试用 -O2)。 在寄存器或易失性中使用 i + + 会导致很大的性能损失(在我的例子中)。

What i did was linking with another empty function which the compiler couldnt see when compiling the "main program"

基本上是这样:

创建“ helper.c”,声明了这个函数(空函数)

void donotoptimize(){}

然后编译成 gcc helper.c -c -o helper.o 然后

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

and link it via gcc my_benchmark.cc helper.o.

这给了我最好的结果(根据我的信念,完全没有开销,但是不能测试,因为没有它我的程序就无法工作:)

我认为它也应该与 icc 一起工作。也许不是如果你启用链接优化,但与 gcc 做。

仅仅使用空的 __asm__语句是不够的: 还要更好地使用数据依赖关系

像这样:

总机

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


}
}

编译和拆卸:

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

产出:

   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上,就像在 在 C + + 中强制执行语句顺序处建议的那样,并产生所需的循环:

这将 i标记为内联程序集的输入和输出。然后,内联汇编是 GCC 的一个黑盒子,它不知道如何修改 i,所以我认为这真的不能被优化掉。

如果对空 __asm__执行相同操作,如下所示:

bad.c

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

it appears to completely remove the loop and outputs:

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

还要注意,__asm__("")__asm__ volatile("")应该是相同的,因为没有输出操作数: 高潮,高潮,挥发性高潮和记忆力衰退的区别

What is happening becomes clearer if we replace it with:

__asm__ volatile("nop");

产生:

   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

So we see that GCC just 卷起的环 the nop loop in this case because the loop was small enough.

因此,如果你依赖于一个空的 __asm__,你将很难预测 GCC 二进制大小/速度的权衡,如果应用最佳,应该总是消除一个空的 __asm__ volatile("");的代码大小为零的循环。

noinline busy loop function

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

将它与 德尼尔森的回答放在一起,一个忙碌的循环函数可以写成:

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 < max2; i++) {
for (unsigned j = 0; j < max; j++) {
__asm__ volatile ("" : "+g" (i), "+g" (j) : :);
}
}
}


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

GitHub 上游。

相关文章:

在 Ubuntu 19.04,GCC 8.3.0中测试。