我怎么知道编译器是否会优化变量?

时间:2020-10-29 13:03:04

标签: c microcontroller

我是微控制器的新手。我已经阅读了很多有关c中volatile变量的文章和文档。我的理解是,在使用volatile时,我们告诉编译器也不要cache来优化变量。但是我什么时候仍然不能真正使用它。
例如,假设我有一个简单的计数器和for循环。

for(int i=0; i < blabla.length; i++) {
    //code here
}

或者当我编写这样的简单代码

int i=1; 
int j=1;
printf("the sum is: %d\n" i+j);

我从不关心此类示例的编译器优化。但是在许多范围内,如果未将变量声明为volatile,则输出将不会达到预期。我怎么知道在其他示例中我必须关心编译器优化?

3 个答案:

答案 0 :(得分:6)

简单的例子:

int flag = 1;

while (flag)
{
   do something that doesn't involve flag
}

可以将其优化为:

while (true)
{
   do something
}

因为编译器知道flag永远不会改变。

使用以下代码:

volatile int flag = 1;

while (flag)
{
   do something that doesn't involve flag
}

什么都不会优化,因为现在编译器知道:“尽管程序不会在flag循环内更改while,但它仍可能会更改。”

答案 1 :(得分:4)

根据cppreference

volatile对象-类型为volatile限定的对象,或volatile对象的子对象,或const-volatile对象的可变子对象。出于优化目的(即在单个执行线程中,通过volatile限定类型的glvalue表达式进行的每次访问(读取或写入操作,成员函数调用等)都被视为可见的副作用)无法优化访问,也不会因其他易见的副作用(在volatile访问之前或之后)而被优化或重新排序,这使得volatile对象适合与信号处理程序进行通信,但不适合与其他执行线程进行通信,请参见std :: memory_order )。任何尝试通过非易失性glvalue(例如,通过对非易失性类型的引用或指针)引用易失性对象都会导致未定义的行为。

这说明了为什么编译器无法进行一些优化,因为它无法完全预测何时在编译时修改其值。此限定符可用于向编译器指示不应进行这些优化,因为它的值可以通过编译器未知的方式进行更改。

我最近没有使用微控制器,但是我认为必须将不同电输入和输出引脚的状态标记为volatile,因为编译器不知道可以在外部进行更改。 (在这种情况下,通过插入组件时的代码以外的其他方式)。

答案 2 :(得分:2)

只需尝试一下。首先是语言和可以进行优化的内容,然后是编译器实际计算出的内容并对其进行优化,如果可以对其进行优化,并不意味着编译器会解决它,也不会始终生成您认为的代码。

Volatile与任何类型的缓存都没有关系,我们不是最近才使用该术语来解决这个问题吗?易失性向编译器指示该变量不应被优化到寄存器中或被优化掉。让我们说对变量的“所有”访问必须返回到内存,尽管不同的编译器对如何使用volatile有不同的理解,但我看到clang(llvm)和gcc(gnu)意见不同,当变量在Windows中两次使用时,一行或类似的东西clang不做两次读取,而只做一次。

这是一个堆栈溢出问题,欢迎您搜索,它的clang代码比gcc稍微快一点,这仅仅是因为由于如何实现volatile的观点不同而导致的一条指令少了。因此,即使在那里,主要的编译器人员也无法就其真正含义达成共识。 C语言的性质,许多实现定义的功能和技巧,请避免在整个编译域中使用它们易失性,位域,联合等。

void fun0 ( void )
{
    unsigned int i;
    unsigned int len;
    len = 5;
    for(i=0; i < len; i++)
    {
    }
}

00000000 <fun0>:
   0:   4770        bx  lr

这是完全无效的代码,它并不表示它什么也不接触,所有项都是本地的,因此可以全部消失,只需返回即可。

unsigned int fun1 ( void )
{
    unsigned int i;
    unsigned int len;
    len = 5;
    for(i=0; i < len; i++)
    {
    }
    return i;
}
00000004 <fun1>:
   4:   2005        movs    r0, #5
   6:   4770        bx  lr

这个返回值,编译器可以判断出它正在计数,循环之后的最后一个值就是返回的值....所以只需返回该值,不需要变量或任何其他代码生成,剩下的就是无效代码。

unsigned int fun2 ( unsigned int len )
{
    unsigned int i;
    for(i=0; i < len; i++)
    {
    }
    return i;
}
00000008 <fun2>:
   8:   4770        bx  lr

类似于fun1,除了将值传递到寄存器中外,恰好与该目标的ABI返回值位于同一寄存器中。因此,在这种情况下,您甚至不必将长度复制到返回值,对于其他体系结构或ABI,我们希望此函数可以优化为return = len并将其发送回去。一个简单的mov指令。

unsigned int fun3 ( unsigned int len )
{
    volatile unsigned int i;
    for(i=0; i < len; i++)
    {
    }
    return i;
}
0000000c <fun3>:
   c:   2300        movs    r3, #0
   e:   b082        sub sp, #8
  10:   9301        str r3, [sp, #4]
  12:   9b01        ldr r3, [sp, #4]
  14:   4298        cmp r0, r3
  16:   d905        bls.n   24 <fun3+0x18>
  18:   9b01        ldr r3, [sp, #4]
  1a:   3301        adds    r3, #1
  1c:   9301        str r3, [sp, #4]
  1e:   9b01        ldr r3, [sp, #4]
  20:   4283        cmp r3, r0
  22:   d3f9        bcc.n   18 <fun3+0xc>
  24:   9801        ldr r0, [sp, #4]
  26:   b002        add sp, #8
  28:   4770        bx  lr
  2a:   46c0        nop         ; (mov r8, r8)

这里有很大的不同,与到目前为止相比,有很多代码。我们想认为volatile表示该变量的所有使用都会触及该变量的内存。

  12:   9b01        ldr r3, [sp, #4]
  14:   4298        cmp r0, r3
  16:   d905        bls.n   24 <fun3+0x18>

获取i并将其与len比较是否小于?我们完成退出循环

  18:   9b01        ldr r3, [sp, #4]
  1a:   3301        adds    r3, #1
  1c:   9301        str r3, [sp, #4]

我小于len,所以我们需要递增,读取,更改,写回。

  1e:   9b01        ldr r3, [sp, #4]
  20:   4283        cmp r3, r0
  22:   d3f9        bcc.n   18 <fun3+0xc>

再次进行i

24: 9801        ldr r0, [sp, #4]

从ram获取我,以便可以将其退回。

所有对i的读取和写入都涉及保存i的内存。因为我们现在要求循环不是死代码,所以必须执行每个迭代才能处理该变量在内存中的所有接触。

void fun4 ( void )
{
    unsigned int a;
    unsigned int b;
    a = 1;
    b = 1;
    fun3(a+b);
}
0000002c <fun4>:
  2c:   2300        movs    r3, #0
  2e:   b082        sub sp, #8
  30:   9301        str r3, [sp, #4]
  32:   9b01        ldr r3, [sp, #4]
  34:   2b01        cmp r3, #1
  36:   d805        bhi.n   44 <fun4+0x18>
  38:   9b01        ldr r3, [sp, #4]
  3a:   3301        adds    r3, #1
  3c:   9301        str r3, [sp, #4]
  3e:   9b01        ldr r3, [sp, #4]
  40:   2b01        cmp r3, #1
  42:   d9f9        bls.n   38 <fun4+0xc>
  44:   9b01        ldr r3, [sp, #4]
  46:   b002        add sp, #8
  48:   4770        bx  lr
  4a:   46c0        nop         ; (mov r8, r8)

这不仅优化了add和a和b变量,而且还通过内联fun3函数进行了优化。

void fun5 ( void )
{
    volatile unsigned int a;
    unsigned int b;
    a = 1;
    b = 1;
    fun3(a+b);
}
0000004c <fun5>:
  4c:   2301        movs    r3, #1
  4e:   b082        sub sp, #8
  50:   9300        str r3, [sp, #0]
  52:   2300        movs    r3, #0
  54:   9a00        ldr r2, [sp, #0]
  56:   9301        str r3, [sp, #4]
  58:   9b01        ldr r3, [sp, #4]
  5a:   3201        adds    r2, #1
  5c:   429a        cmp r2, r3
  5e:   d905        bls.n   6c <fun5+0x20>
  60:   9b01        ldr r3, [sp, #4]
  62:   3301        adds    r3, #1
  64:   9301        str r3, [sp, #4]
  66:   9b01        ldr r3, [sp, #4]
  68:   429a        cmp r2, r3
  6a:   d8f9        bhi.n   60 <fun5+0x14>
  6c:   9b01        ldr r3, [sp, #4]
  6e:   b002        add sp, #8
  70:   4770        bx  lr

还内联了fun3,但是每次都会从内存中读取a变量 而不是被优化

  58:   9b01        ldr r3, [sp, #4]
  5a:   3201        adds    r2, #1


void fun6 ( void )
{
    unsigned int i;
    unsigned int len;
    len = 5;
    for(i=0; i < len; i++)
    {
        fun3(i);
    }
}
00000074 <fun6>:
  74:   2300        movs    r3, #0
  76:   2200        movs    r2, #0
  78:   2100        movs    r1, #0
  7a:   b082        sub sp, #8
  7c:   9301        str r3, [sp, #4]
  7e:   9b01        ldr r3, [sp, #4]
  80:   3201        adds    r2, #1
  82:   9b01        ldr r3, [sp, #4]
  84:   2a05        cmp r2, #5
  86:   d00d        beq.n   a4 <fun6+0x30>
  88:   9101        str r1, [sp, #4]
  8a:   9b01        ldr r3, [sp, #4]
  8c:   4293        cmp r3, r2
  8e:   d2f7        bcs.n   80 <fun6+0xc>
  90:   9b01        ldr r3, [sp, #4]
  92:   3301        adds    r3, #1
  94:   9301        str r3, [sp, #4]
  96:   9b01        ldr r3, [sp, #4]
  98:   429a        cmp r2, r3
  9a:   d8f9        bhi.n   90 <fun6+0x1c>
  9c:   3201        adds    r2, #1
  9e:   9b01        ldr r3, [sp, #4]
  a0:   2a05        cmp r2, #5
  a2:   d1f1        bne.n   88 <fun6+0x14>
  a4:   b002        add sp, #8
  a6:   4770        bx  lr

基于我对gnu的经验感到困惑,我发现这很有趣,可以对其进行更好的优化,但是正如所指出的那样,您可以期待一件事,但是编译器会执行它。

  9c:   3201        adds    r2, #1
  9e:   9b01        ldr r3, [sp, #4]
  a0:   2a05        cmp r2, #5

出于某种原因,fun6函数中的i变量被放在堆栈中,它不是易失性的,它并不希望每次都进行这种访问。但这就是他们的实现方式。

如果我使用旧版本的gcc构建,我会看到

9c:3201添加r2#1 9e:9b01 ldr r3,[sp,#4] a0:2a05 cmp r2,#5

要注意的另一件事是,至少每个版本的gnu都不会变好,有时甚至会变差,这是一个简单的情况。

void fun7 ( void )
{
    unsigned int i;
    unsigned int len;
    len = 5;
    for(i=0; i < len; i++)
    {
        fun2(i);
    }
}
0000013c <fun7>:
 13c:   e12fff1e    bx  lr

好吧,太极端了(结果并不令人惊讶),让我们尝试一下

void more_fun ( unsigned int );
void fun8 ( void )
{
    unsigned int i;
    unsigned int len;
    len = 5;
    for(i=0; i < len; i++)
    {
        more_fun(i);
    }
}

000000ac <fun8>:
  ac:   b510        push    {r4, lr}
  ae:   2000        movs    r0, #0
  b0:   f7ff fffe   bl  0 <more_fun>
  b4:   2001        movs    r0, #1
  b6:   f7ff fffe   bl  0 <more_fun>
  ba:   2002        movs    r0, #2
  bc:   f7ff fffe   bl  0 <more_fun>
  c0:   2003        movs    r0, #3
  c2:   f7ff fffe   bl  0 <more_fun>
  c6:   2004        movs    r0, #4
  c8:   f7ff fffe   bl  0 <more_fun>
  cc:   bd10        pop {r4, pc}
  ce:   46c0        nop         ; (mov r8, r8)

选择5低于某个阈值就选择展开它就不足为奇了。

void fun9 ( unsigned int len )
{
    unsigned int i;
    for(i=0; i < len; i++)
    {
        more_fun(i);
    }
}
000000d0 <fun9>:
  d0:   b570        push    {r4, r5, r6, lr}
  d2:   1e05        subs    r5, r0, #0
  d4:   d006        beq.n   e4 <fun9+0x14>
  d6:   2400        movs    r4, #0
  d8:   0020        movs    r0, r4
  da:   3401        adds    r4, #1
  dc:   f7ff fffe   bl  0 <more_fun>
  e0:   42a5        cmp r5, r4
  e2:   d1f9        bne.n   d8 <fun9+0x8>
  e4:   bd70        pop {r4, r5, r6, pc}

这就是我想要的。因此,在这种情况下,i变量位于寄存器(r4)中,而不是位于堆栈上,如上所示。调用约定说r4及其后的其他一些字符(r5,r6,...)必须保留。这是调用优化器看不到的外部函数,因此它必须实现循环,以便按顺序依次调用每个值多次。不是无效代码。

教科书/教室意味着局部变量在堆栈中,但不一定必须存在。我没有被声明为易失性,所以取一个非易失性寄存器,r4将其保存在堆栈上,以便调用者不会丢失其状态,将r4用作i,被调用者函数more_fun要么不会触摸它,要么会返回找到的结果它。您添加了一个推送,但是在循环中保存了一堆负载和存储,这是基于目标和ABI的另一种优化。

Volatile是对编译器的建议/建议/期望,它具有该变量的地址,并在使用时执行实际的加载和存储对该变量的访问。理想的用例是,例如当您在硬件的外围设备中有一个控制/状态寄存器时,您需要代码中描述的所有访问都以编码顺序进行,而无需优化。对于与语言无关的高速缓存,您必须设置高速缓存和mmu或其他解决方案,以使控制和状态寄存器不会被高速缓存,并且在我们希望触摸外设时不会对其进行触摸。在这两层中,您都需要告诉编译器进行所有访问,并且不需要在内存系统中阻止这些访问。

在没有波动的情况下,并且根据使用的命令行选项以及经过优化的列表,已对编译器进行了编程以尝试执行编译器,这些尝试将按照在编译器代码中进行编程的方式进行尝试。如果编译器由于不在优化域中而无法查看上面的more_fun之类的调用函数,则编译器必须在功能上按顺序表示所有调用,如果可以看到并且允许内联,则编译器可以通过编程来做到因此本质上将函数与调用方内联,然后优化整个Blob,就好像它是基于其他可用选项的一个函数一样。由于其性质而使得被调用方函数变大并不罕见,但是当调用方传递特定值并且编译器可以看到所有这些值时,调用方和被调用方代码可以比被调用方实现小。

您经常会看到人们想要例如通过检查编译器的输出来学习汇编语言,例如:

void fun10 ( void )
{
    int a;
    int b;
    int c;
    a = 5;
    b = 6;
    c = a + b;
}

没意识到那是无效代码,如果使用了优化器,则应该对其进行优化,他们问一个堆栈溢出问题,有人说您需要关闭优化器,现在您需要处理很多负载,并且存储必须了解并跟踪堆栈偏移量,当它是有效的asm代码时,您可以研究它不是您想要的,而是类似的东西对于这种工作更有价值

unsigned int fun11 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

编译器不知道输入,并且需要返回值,这样它就不会死掉必须执行的代码。

这是一个简单的案例,表明呼叫者加上被呼叫者小于被呼叫者

000000ec <fun11>:
  ec:   1840        adds    r0, r0, r1
  ee:   4770        bx  lr

000000f0 <fun12>:
  f0:   2007        movs    r0, #7
  f2:   4770        bx  lr

虽然看起来不太简单,但它已内联了代码,但它优化了a = 3,b = 4分配,优化了加法运算,并简单地预先计算了结果并返回了结果。

当然,使用gcc,您可以选择要添加或阻止的优化,其中有一些清单可供您研究。

通过很少的练习,您至少可以在函数视图中看到可优化的内容,然后希望编译器将其找出来。当然,可视化内联会花费更多的工作,但实际上与您在视觉上内联的一样。

现在,有一些方法可以使用gnu和llvm在整个文件中进行优化,基本上整个项目都是这样,因此more_fun现在将可见,并且调用该函数的功能可能会比在调用者的一个文件的对象中看到的进一步优化。在编译和/或链接上使用某些命令行以使其起作用,我没有记住它们。借助llvm,可以合并字节码,然后对其进行优化,但它并不能始终如您所愿地完成整个项目优化。

相关问题