我是微控制器的新手。我已经阅读了很多关于 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: %dn" i+j);
我从不关心此类示例的编译器优化。但是在许多作用域中,如果变量没有声明为volatile
则 ouptut 将不会按预期进行。我怎么知道我必须关心其他示例中的编译器优化?
简单的例子:
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
,但无论如何它都可能会更改"。
根据 cpp 偏好:
易失性对象 - 类型为易失性限定的对象,易失性对象的子对象,或常量易失性对象的可变子对象。出于优化目的,通过易失性限定类型的 glvalue 表达式进行的每次访问(读取或写入操作、成员函数调用等)都被视为可见的副作用(即,在单个执行线程中,易失性访问不能使用在易失性访问之前或之后排序的另一个可见副作用进行优化或重新排序。这使得易失性对象适合与信号处理程序通信,但不适合与另一个执行线程通信,请参阅 std::memory_order)。任何通过非易失性 gl值(例如,通过引用或指向非易失性类型的指针)引用易失性对象的尝试都会导致未定义的行为。
这就解释了为什么编译器无法进行某些优化,因为它无法完全预测何时在编译时修改其值。此限定符可用于向编译器指示它不应执行这些优化,因为编译器可以以未知的方式更改其值。
我最近没有使用微控制器,但我认为不同电输入和输出引脚的状态必须标记为volatile
,因为编译器不知道它们可以在外部更改。(在这种情况下,通过代码以外的方式,例如插入组件时)。
试试吧。 首先是语言和可以优化的内容,然后是编译器实际计算和优化的内容,如果可以优化并不意味着编译器会弄清楚它,也不会总是产生你认为的代码。
易失性与任何类型的缓存无关,我们最近不是刚刚使用该术语得到这个问题吗? 易失性向编译器指示变量不应优化为寄存器或优化掉。 假设对该变量的"所有"访问必须返回到内存,尽管不同的编译器对如何使用易失性有不同的理解,但我看到 clang (llvm) 和 gcc (gnu) 不同意,当变量连续使用两次或类似的东西时,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>
得到我并将其与 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的所有读写都涉及保存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,...)。 这是在调用优化器看不到的外部函数,因此它必须实现循环,以便按顺序调用每个值多次调用该函数。 不是死代码。
教科书/课堂意味着局部变量在堆栈上,但它们不必在堆栈上。 i 没有声明为易失性,因此取而代之的是非易失寄存器,R4 将其保存在堆栈上,以便调用者不会丢失其状态,使用 R4 作为 i 和被调用方函数more_fun要么不会碰它,要么会在找到它时返回它。您添加推送,但在循环中保存一堆加载和存储,这是基于目标和 ABI 的另一种优化。
易失性是编译器的建议/建议/愿望,即它具有变量的地址,并在使用时执行对该变量的实际加载和存储访问。 理想情况下,例如当您在硬件的外设中具有控制/状态寄存器时,您需要按编码顺序进行代码中描述的所有访问,而无需优化。对于独立于语言的缓存,您必须设置缓存和 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 有一种方法可以合并字节码然后对其进行优化,但就整个项目优化而言,它并不总是像您希望的那样。