我怎样才能防止GCC优化出一个忙碌的等待循环?

fcg9iug3  于 2024-01-08  发布在  其他
关注(0)|答案(9)|浏览(216)

我想为Atmel AVR微控制器编写一个C代码固件。我将使用GCC编译它。此外,我想启用编译器优化(-Os-O2),因为我认为没有理由不启用它们,而且它们可能会比手动编写汇编程序更快地生成更好的汇编程序。
但是我想要一小段没有优化的代码,我想延迟一段时间执行一个函数,因此我想写一个什么都不做的循环,只是为了浪费一些时间。不需要精确,只是等待一些时间。

  1. /* How can it NOT optimize this, while optimizing other code? */
  2. unsigned char i, j;
  3. j = 0;
  4. while(--j) {
  5. i = 0;
  6. while(--i);
  7. }

字符串
由于AVR中的内存访问要慢得多,我希望ij保留在CPU寄存器中。
我从AVR Libc中找到了util/delay.hutil/delay_basic.h。虽然大多数时候使用这些函数可能是一个更好的主意,但这个问题仍然有效和有趣。
相关问题:

zpqajqem

zpqajqem1#

我在从dmckee的答案中找到一个链接后开发了这个答案,但它采用了与他/她的答案不同的方法。
来自GCC的Function Attributes文档提到:
noinline此函数属性可防止考虑将函数内联。如果函数没有副作用,则存在内联以外的优化,会导致函数调用被优化掉,尽管函数调用是实时的。若要防止此类调用被优化掉,请将asm ("");
这给了我一个有趣的想法.我尝试在内部循环中添加一个空的汇编代码,而不是在内部循环中添加nop指令,如下所示:

  1. unsigned char i, j;
  2. j = 0;
  3. while(--j) {
  4. i = 0;
  5. while(--i)
  6. asm("");
  7. }

字符串
这个循环没有被优化,也没有插入额外的nop指令。
更重要的是,如果你使用volatile,gcc会将这些变量存储在RAM中,并添加一堆lddstd来将它们复制到临时寄存器中。另一方面,这种方法不使用volatile,也不会产生这样的开销。

此外,如果 *assembly语句必须在我们放置它的位置执行(即不能作为优化移出循环),则也可以使用__asm__ __volatile__("")

展开查看全部
nwwlzxa7

nwwlzxa72#

ij变量解析为volatile。这将阻止编译器优化包含这些变量的代码。

  1. unsigned volatile char i, j;

字符串

jdgnovmf

jdgnovmf3#

空的__asm__语句是不够的:最好使用数据依赖关系

就像这样:

文件 main.c

  1. int main(void) {
  2. unsigned i;
  3. for (i = 0; i < 10; i++) {
  4. __asm__ volatile("" : "+g" (i) : :);
  5. }
  6. }

字符串
编译和反汇编:

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


输出量:

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


我相信这是健壮的,因为它在循环变量i上放置了一个显式的数据依赖项,如:Enforcing statement order in C++所示,并产生了所需的循环:
这就把i标记为内联汇编的输入和输出,那么,内联汇编对于GCC来说就是一个黑盒子,GCC不知道它是如何修改i的,所以我认为这一点真的无法优化掉。
如果我对一个空的__asm__执行相同的操作,如下所示:

文件 * 不正确.c*

  1. int main(void) {
  2. unsigned i;
  3. for (i = 0; i < 10; i++) {
  4. __asm__ volatile("");
  5. }
  6. }


它看起来完全消除了循环和输出:

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


另请注意,__asm__("")__asm__ volatile("")应该相同,因为没有输出操作数:The difference between asm, asm volatile and clobbering memory
如果我们将其替换为:

  1. __asm__ volatile("nop");


其产生:

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


我们可以看到,GCC在本例中只是loop unrollednop循环,因为该循环足够小。
因此,如果您依赖于一个空的__asm__,您将依赖于难以预测的GCC二进制大小/速度权衡,如果以最佳方式应用,应该总是会删除代码大小为零的空__asm__ volatile("");的循环。

noinline忙碌循环功能

如果在编译时不知道循环的大小,那么完全展开是不可能的,但是GCC仍然可以决定以块的形式展开,这将使延迟不一致。
将其与Denilson's answer结合在一起,一个忙碌循环函数可以写成:

  1. void __attribute__ ((noinline)) busy_loop(unsigned max) {
  2. for (unsigned i = 0; i < max; i++) {
  3. __asm__ volatile("" : "+g" (i) : :);
  4. }
  5. }
  6. int main(void) {
  7. busy_loop(10);
  8. }


其在以下位置拆卸:

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


这里需要使用volatile将程序集标记为可能有副作用,因为在本例中我们有一个输出变量。
双循环版本可以是:

  1. void __attribute__ ((noinline)) busy_loop(unsigned max, unsigned max2) {
  2. for (unsigned i = 0; i < max2; i++) {
  3. for (unsigned j = 0; j < max; j++) {
  4. __asm__ volatile ("" : "+g" (i), "+g" (j) : :);
  5. }
  6. }
  7. }
  8. int main(void) {
  9. busy_loop(10, 10);
  10. }


GitHub upstream的一个。
相关主题:

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

展开查看全部
ztyzrc3y

ztyzrc3y4#

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

7lrncoxx

7lrncoxx5#

我不知道编译器的AVR版本是否支持full set of #pragma s(链接中有趣的内容都来自GCC版本4.4),但这是您通常会开始的地方。

xwmevbvl

xwmevbvl6#

对我来说,在GCC 4.7.0上,空的汇编代码无论如何都是用-O3优化的(我没有尝试用-O2)。在 registervolatile 中使用i++会导致很大的性能损失(在我的情况下)。
我链接了另一个空函数,编译器在编译“主程序”时看不到它。
基本上是这样的:
我创建了“helper.c”,并声明了这个函数(空函数):

  1. void donotoptimize(){}

字符串
然后编译gcc helper.c -c -o helper.o

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


并通过gcc my_benchmark.cc helper.o连接。
这给了我最好的结果(从我的信念,没有开销,但我不能测试,因为我的程序将无法工作没有它:))
我认为它也应该能在ICC上工作。如果你启用了链接优化,也许不行,但在GCC上可以。

展开查看全部
scyqe7ek

scyqe7ek7#

把volatile asm放在这里应该会有帮助。你可以在这里阅读更多:

如果你在Windows上工作,你甚至可以尝试将代码放在pragmas下,如下所述:
https://www.securecoding.cert.org/confluence/display/cplusplus/MSC06-CPP.+Be+aware+of+compiler+optimization+when+dealing+with+sensitive+data

xt0899hw

xt0899hw8#

把这个循环放在一个单独的.c文件中,不要优化那个文件。更好的是,用汇编程序编写这个例程,然后从C调用它。无论哪种方式,优化器都不会参与进来。
我有时会做volatile的事情,但通常会创建一个asm函数,它只是返回对该函数的put调用,优化器会使 for/while 循环紧密,但它不会优化它,因为它必须对dummy函数进行所有调用。Denilson Sá的nop答案做同样的事情,但更紧密。

qybjjes1

qybjjes19#

你也可以使用register关键字。使用register声明的变量存储在CPU寄存器中。
在您的案例中:

  1. register unsigned char i, j;
  2. j = 0;
  3. while(--j) {
  4. i = 0;
  5. while(--i);
  6. }

字符串

相关问题