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

How do i know if the compiler will optimize a variable?

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

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,则输出不会如预期的那样。我怎么知道我必须关心其他示例中的编译器优化?

根据cppreference

volatile object - an object whose type is volatile-qualified, or a subobject of a volatile object, or a mutable subobject of a const-volatile object. Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order). Any attempt to refer to a volatile object through a non-volatile glvalue (e.g. through a reference or pointer to non-volatile type) results in undefined behavior.

这解释了为什么编译器无法进行某些优化,因为它无法完全预测其值何时会在 compile-time 处被修改。此限定符可用于向编译器指示它不应进行这些优化,因为它的值可以以编译器未知的方式更改。

我最近没有使用微控制器,但我认为不同电气输入和输出引脚的状态必须标记为 volatile,因为编译器不知道它们可以从外部更改。 (在这种情况下,通过代码以外的方式,例如 plug-in 一个组件)。

简单示例:

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
}

什么都不会被优化,因为现在编译器知道:“虽然程序在 while 循环内没有改变 flag,但无论如何它都可能改变”。

试试吧。首先是语言和可以优化的内容,然后是编译器实际计算和优化的内容,如果可以优化并不意味着编译器会计算出来,也不会总是产生您认为的代码。

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

Stack Overflow问题,欢迎搜索,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

这是完全无效的代码,它没有注意到它什么都没有,所有项目都是本地的,所以它可以全部消失,只需 return.

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

这个 return 是什么东西,编译器可以计算出它正在计数,循环后的最后一个值就是 returned....所以只是 return该值,不需要变量或任何其他代码生成,剩下的就是死代码。

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 的 return 值是同一个寄存器。因此,在这种情况下,您甚至不必将长度复制到 return 值,对于其他架构或 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]

i 小于 len 所以我们需要增加它,读取它,改变它,写回它。

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

再做i < len测试,看是小于还是大于,再循环还是不循环。

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

从 ram 中获取 i 以便可以 returned.

所有对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)

这既优化了加法和 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,...)必须保留。这是在调用一个优化器看不到的外部函数,所以它必须实现循环,以便函数被调用多次,每个值都按顺序调用。不是死代码。

Textbook/classroom 意味着局部变量在堆栈上,但它们不是必须的。 i 没有声明为 volatile,所以取一个 non-volatile 寄存器,r4 将其保存在堆栈中,这样调用者就不会丢失其状态,将 r4 用作 i 并且被调用函数 more_fun 要么不会触及它,要么将 return 它找到它。您添加了一个推送,但在循环中保存了一堆加载和存储,这是另一个基于目标和 ABI 的优化。

Volatile 是一个 suggestion/recommendation/desire编译器知道它有变量的地址,并在使用时执行对该变量的实际加载和存储访问。理想情况下,当您在硬件的外围设备中有一个 control/status 寄存器时,您需要代码中描述的所有访问按编码顺序发生,无需优化。至于独立于语言的缓存,您必须设置缓存和 mmu 或其他解决方案,以便控制和状态寄存器不会被缓存,并且当我们希望它被触摸时,外设也不会被触摸。采用两层,您需要告诉编译器执行所有访问,并且不需要在内存系统中阻止这些访问。

没有 volatile 并且基于您使用的命令行选项和优化列表,编译器已被编程为尝试执行编译器将尝试执行那些在编译器代码中编程的优化。如果编译器无法看到像上面 more_fun 这样的调用函数,因为它不在这个优化域中,那么编译器必须在功能上按顺序表示所有调用,如果它可以看到并且允许内联,那么编译器可以如果被编程为这样做,本质上是将函数与调用者内联,然后优化整个 blob,就好像它是一个基于其他可用选项的函数一样。由于其性质,被调用函数体积庞大的情况并不少见,但是当调用者传递特定值并且编译器可以看到所有这些值时,调用者加上被调用者代码可能比被调用者实现更小。

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

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

没有意识到那是死代码,如果使用优化器应该被优化掉,他们问了一个 Stack Overflow 问题,有人说你需要关闭优化器,现在你得到了很多负载和存储了解和跟踪堆​​栈偏移量,虽然它是有效的 asm 代码,但您可以研究它,这不是您所希望的,而像这样的东西对这项工作更有价值

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

编译器不知道输入,需要一个 return 值,因此它不能死代码,它必须实现它。

这是一个演示调用者加被调用者小于被调用者的简单案例

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 赋值,优化了加法运算并简单地 pre-computed 结果和 return编辑它。

当然,使用 gcc,您可以挑选要添加或阻止的优化,这里有一份清单供您研究。

通过很少的练习,您至少可以在函数的视图中看到什么是可优化的,然后希望编译器能够解决它。当然可视化内联需要更多的工作,但实际上它是一样的,你只是视觉内联它。

现在有 gnu 和 llvm 跨文件优化的方法,基本上是整个项目,所以 more_fun 现在可以看到,调用它的函数可能比你在对象中看到的更优化与来电者的一​​个文件。在 compile and/or link 上使用某些命令行使其工作,但我没有记住它们。使用 llvm 有一种方法可以合并字节码然后对其进行优化,但就整个项目优化而言,它并不总是能达到您希望的效果。