以下代码包含中断服务例程和常规函数func()
,该函数使用全局flag
和静态全局g
。由于flag
是异步修改的,因此在没有任何内存屏障的情况下,此代码是错误的。
引入全局内存屏障<1>修复,但也抑制了对g
的优化。我的期望是对g
的所有访问都将得到优化,因为g
在此TU之外是不可访问的。
我知道一个全局内存条和调用一个非内联函数f()
的效果是一样的<3>,但是这里有一个相同的问题:既然g
在此TU之外不可见,为什么不优化对g
的访问?
我尝试使用一个特定的内存屏障来对付flag
,但也无济于事。
(我避免将flag
限定为volatile
:这在这里会有帮助,但它只应用于访问HW寄存器)。
现在的问题是如何优化对g
的访问?
编译器:avr-gcc
https://godbolt.org/z/ob6YoKx5a
#include <stdint.h>
uint8_t flag;
void isr() __asm__("__vector_5") __attribute__ ((__signal__, __used__, __externally_visible__));
void isr() {
flag = 1;
}
static uint8_t g;
void f();
void func(void) {
for (uint8_t i=0; i<20; i++) {
// f(); // <3>
// __asm__ __volatile__ ("" : : : "memory"); // <1>
// __asm__ __volatile__ ("" : "=m" (flag)); // <2>
++g;
if (flag) {
flag = 0;
}
}
}
//void f(){}
3条答案
按热度按时间csbfibhn1#
很多误解。
“global”--这不是一个正式的术语,是一个在文件作用域声明的变量,它带有外部链接,可以被程序的其他部分使用
extern
引用。这几乎总是糟糕的做法和糟糕的设计。static
确保变量无论在何处声明都具有内部链接。因此,根据定义,它不是“全局”变量。声明为static
的变量只能在声明它们的作用域中访问。有关详细信息,请查看What does the static keyword do in C?这是一个单核AVR,仍然在生产的最简单的CPU之一,所以内存屏障不是一个适用的概念。不要阅读关于64位x86上的PC编程的文章,并试图将它们应用到20世纪90年代的8位传统架构上。错误的工具,错误的目的,错误的系统。
volatile
不一定会充当内存屏障,即使在该概念适用的系统上也是如此。例如,请参见https://stackoverflow.com/a/58697222/584518。volatile
不会在任何系统(包括AVR)上实现代码可重入/线程安全/中断安全。volatile
在此上下文中的用途是当编译器没有意识到ISR是由硬件而不是由程序调用时,防止不正确的编译器优化。我们不能用volatile
限定C中的函数或代码,只能限定对象,因此与ISR共享的变量需要用volatile
限定。详细信息和示例如下:https://electronics.stackexchange.com/questions/409545/using-volatile-in-embedded-c-development/409570#409570至于你应该做些什么,我相信我对你另一个问题的回答已经涵盖了这一点。
lb3vh1jj2#
__asm__ __volatile__ ("" :: "m" (flag):"memory"); // <2>
由于
"memory"
,您正在占用所有内存。如果您想表示只有
flag
发生了更改(并且更改具有volatile效应),则:__asm__ __volatile__ ("" : "+m" (flag));
这告诉GCC
flag
被更改了,而不仅仅是像中那样的asm输入<2>。hjzp0vay3#
@Lundin是正确的,
if (flag) flag = 0;
不是原子RMW,甚至不会与volatile
(仍然只是分离的原子加载和原子存储;他们之间可能会发生中断。)查看他们的回答以了解更多信息,对于某些目标来说,这似乎是根本错误的方法。此外,只有当您用_Atomic
替换volatile
时,避免使用volatile
才有意义;这就是你链接的Herb Sutter's 2009 article所表达的意思,并不是说你应该使用普通变量,通过屏障强制内存访问;也就是fraught with peril,因为编译器可以发明对非原子变量的加载或存储,以及其他不太明显的缺陷。如果你打算用内联asm滚动你自己的原子,你需要volatile
;GCC和Clang支持volatile
的这种用法,因为这是Linux内核以及C11之前的代码所做的事情。优化掉
g
障碍并不是GCC不能完全优化
g
的原因,当有一个像void func(){g++;}
这样简单的函数,文件中除了声明之外没有其他代码时,GCC会错过这种有效的优化。但是,如果使用
g
的C代码不会在调用之间产生一系列不同的值,那么即使使用asm("" ::: "memory")
,g
也会被优化掉。存储一个常量是很好的,而在一个增量之后存储一个常量就足以使增量成为一个死存储。也许GCC优化它的启发式算法只考虑了几个调用的链,而不去证明没有价值相关的东西发生Godbolt上AVR的GCC 12-O3输出:
"memory"
clobber强制编译器假设g
的值可能已经改变,如果它 * 不 * 优化掉它的话。但是它没有使编译中的所有static
变量都隐式输入/输出。asm
语句是隐式volatile的,因为它没有输出操作数。告诉编译器只有
flag
被读写应该是等价的。除非g
没有被优化掉,否则GCC可以将g
的负载提升到循环之外,只存储递增的值。(它错过了将存储从循环中下沉的优化。这是法律的的;"+m"(flag)
操作数告诉编译器,flag
已被读取和写入,因此现在可以具有任何值,但如果没有"memory"
清除器,编译器可以假定asm
语句没有从寄存器或内存读取或写入C抽象机的任何其他状态。带有
"=m" (flag)
仅输出操作数的语句不同:它告诉编译器flag
的旧值是不相关的,不是输入,所以如果它正在展开循环,在asm
语句之前对flag
的任何存储都将是死存储。(The
asm
语句是易失性的,因此它必须运行它在抽象机中到达的次数;它必须假设可能会有一些副作用,比如对非C变量的I/O,所以前面的asm语句不能因此而被删除,而只能是因为volatile
。)