我是微控制器的新手。我已经阅读了很多有关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
,则输出将不会达到预期。我怎么知道在其他示例中我必须关心编译器优化?
答案 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 从ram获取我,以便可以将其退回。 所有对i的读取和写入都涉及保存i的内存。因为我们现在要求循环不是死代码,所以必须执行每个迭代才能处理该变量在内存中的所有接触。 这不仅优化了add和a和b变量,而且还通过内联fun3函数进行了优化。 还内联了fun3,但是每次都会从内存中读取a变量
而不是被优化 基于我对gnu的经验感到困惑,我发现这很有趣,可以对其进行更好的优化,但是正如所指出的那样,您可以期待一件事,但是编译器会执行它。 出于某种原因,fun6函数中的i变量被放在堆栈中,它不是易失性的,它并不希望每次都进行这种访问。但这就是他们的实现方式。 如果我使用旧版本的gcc构建,我会看到 9c:3201添加r2#1
9e:9b01 ldr r3,[sp,#4]
a0:2a05 cmp r2,#5 要注意的另一件事是,至少每个版本的gnu都不会变好,有时甚至会变差,这是一个简单的情况。 好吧,太极端了(结果并不令人惊讶),让我们尝试一下 选择5低于某个阈值就选择展开它就不足为奇了。 这就是我想要的。因此,在这种情况下,i变量位于寄存器(r4)中,而不是位于堆栈上,如上所示。调用约定说r4及其后的其他一些字符(r5,r6,...)必须保留。这是调用优化器看不到的外部函数,因此它必须实现循环,以便按顺序依次调用每个值多次。不是无效代码。 教科书/教室意味着局部变量在堆栈中,但不一定必须存在。我没有被声明为易失性,所以取一个非易失性寄存器,r4将其保存在堆栈上,以便调用者不会丢失其状态,将r4用作i,被调用者函数more_fun要么不会触摸它,要么会返回找到的结果它。您添加了一个推送,但是在循环中保存了一堆负载和存储,这是基于目标和ABI的另一种优化。 Volatile是对编译器的建议/建议/期望,它具有该变量的地址,并在使用时执行实际的加载和存储对该变量的访问。理想的用例是,例如当您在硬件的外围设备中有一个控制/状态寄存器时,您需要代码中描述的所有访问都以编码顺序进行,而无需优化。对于与语言无关的高速缓存,您必须设置高速缓存和mmu或其他解决方案,以使控制和状态寄存器不会被高速缓存,并且在我们希望触摸外设时不会对其进行触摸。在这两层中,您都需要告诉编译器进行所有访问,并且不需要在内存系统中阻止这些访问。 在没有波动的情况下,并且根据使用的命令行选项以及经过优化的列表,已对编译器进行了编程以尝试执行编译器,这些尝试将按照在编译器代码中进行编程的方式进行尝试。如果编译器由于不在优化域中而无法查看上面的more_fun之类的调用函数,则编译器必须在功能上按顺序表示所有调用,如果可以看到并且允许内联,则编译器可以通过编程来做到因此本质上将函数与调用方内联,然后优化整个Blob,就好像它是基于其他可用选项的一个函数一样。由于其性质而使得被调用方函数变大并不罕见,但是当调用方传递特定值并且编译器可以看到所有这些值时,调用方和被调用方代码可以比被调用方实现小。 您经常会看到人们想要例如通过检查编译器的输出来学习汇编语言,例如: 没意识到那是无效代码,如果使用了优化器,则应该对其进行优化,他们问一个堆栈溢出问题,有人说您需要关闭优化器,现在您需要处理很多负载,并且存储必须了解并跟踪堆栈偏移量,当它是有效的asm代码时,您可以研究它不是您想要的,而是类似的东西对于这种工作更有价值 编译器不知道输入,并且需要返回值,这样它就不会死掉必须执行的代码。 这是一个简单的案例,表明呼叫者加上被呼叫者小于被呼叫者 虽然看起来不太简单,但它已内联了代码,但它优化了a = 3,b = 4分配,优化了加法运算,并简单地预先计算了结果并返回了结果。 当然,使用gcc,您可以选择要添加或阻止的优化,其中有一些清单可供您研究。 通过很少的练习,您至少可以在函数视图中看到可优化的内容,然后希望编译器将其找出来。当然,可视化内联会花费更多的工作,但实际上与您在视觉上内联的一样。 现在,有一些方法可以使用gnu和llvm在整个文件中进行优化,基本上整个项目都是这样,因此more_fun现在将可见,并且调用该函数的功能可能会比在调用者的一个文件的对象中看到的进一步优化。在编译和/或链接上使用某些命令行以使其起作用,我没有记住它们。借助llvm,可以合并字节码,然后对其进行优化,但它并不能始终如您所愿地完成整个项目优化。24: 9801 ldr r0, [sp, #4]
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)
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
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
9c: 3201 adds r2, #1
9e: 9b01 ldr r3, [sp, #4]
a0: 2a05 cmp r2, #5
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)
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}
void fun10 ( void )
{
int a;
int b;
int c;
a = 5;
b = 6;
c = a + b;
}
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