76

我想为 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);
}

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


更新:我刚刚从AVR Libc找到了util/delay.hutil/delay_basic.h。尽管大多数时候使用这些函数可能是一个更好的主意,但这个问题仍然有效且有趣。


相关问题:

4

9 回答 9

89

在遵循dmckee's answer的链接后,我开发了这个答案,但它采用了与他/她的答案不同的方法。

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

noinline 此函数属性可防止将函数考虑进行内联。如果函数没有副作用,那么除了内联之外还有其他优化会导致函数调用被优化掉,尽管函数调用是实时的。为了防止这样的调用被优化掉,把asm ("");

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

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

它奏效了!该循环尚未优化,也没有nop插入额外的指令。

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


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

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

于 2011-08-16T19:55:34.360 回答
29

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

unsigned volatile char i, j;
于 2011-08-16T19:30:00.560 回答
10

__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如下所示:Enforcing statement order in C++ and generate the desired loop:

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

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

坏的.c

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

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

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

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

如果我们将其替换为:

__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

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

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

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);
}

在以下位置拆卸:

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 中测试。

于 2019-11-07T23:07:18.237 回答
7

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

于 2011-09-01T20:31:06.933 回答
3

我不知道编译器的 avr 版本是否支持完整的#pragmas集(链接中有趣的都来自 gcc 版本 4.4),但这是您通常开始的地方。

于 2011-08-16T19:30:32.823 回答
3

对我来说,在 GCC 4.7.0 上,无论如何都使用 -O3 优化了空 asm(没有尝试使用 -O2)。在 register 或 volatile 中使用 i++ 会导致很大的性能损失(在我的情况下)。

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

基本上是这样的:

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

void donotoptimize(){}

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

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

并通过链接gcc my_benchmark.cc helper.o

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

我认为它也应该与 icc 一起使用。如果您启用链接优化,可能不会,但使用 gcc 可以。

于 2014-01-18T20:23:38.097 回答
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

希望这可以帮助。

于 2011-08-17T19:18:34.267 回答
0

将该循环放在单独的 .c 文件中,不要优化该文件。最好在汇编程序中编写该例程并从 C 中调用它,无论哪种方式优化器都不会参与。

我有时会做 volatile 的事情,但通常会创建一个 asm 函数,它只返回对该函数的调用,优化器将使 for/while 循环紧密,但它不会优化它,因为它必须对虚拟函数进行所有调用。Denilson Sá 的 nop 回答做了同样的事情,但更严格......

于 2011-08-16T22:16:06.283 回答
-1

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

在你的情况下:

register unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i);
}
于 2012-09-06T13:14:19.510 回答