C语言 为什么内存障碍会阻碍静态全局变量的优化?

li9yvcax  于 2023-02-11  发布在  其他
关注(0)|答案(3)|浏览(144)

以下代码包含中断服务例程和常规函数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(){}
csbfibhn

csbfibhn1#

很多误解。

  • 没有什么叫“静态全局”,就像说“狗猫”一样,它们是彼此的对立面。你可以在局部作用域或文件作用域声明变量。你可以有内部链接或外部链接的变量。

“global”--这不是一个正式的术语,是一个在文件作用域声明的变量,它带有外部链接,可以被程序的其他部分使用extern引用。这几乎总是糟糕的做法和糟糕的设计。
static确保变量无论在何处声明都具有内部链接。因此,根据定义,它不是“全局”变量。声明为static的变量只能在声明它们的作用域中访问。有关详细信息,请查看What does the static keyword do in C?

  • 内存屏障的概念在多核系统之外没有多大意义。内存屏障的目的是防止多核系统中的并发执行/流水线、预取或指令重排序。此外,内存屏障不能保证软件级别的重入。

这是一个单核AVR,仍然在生产的最简单的CPU之一,所以内存屏障不是一个适用的概念。不要阅读关于64位x86上的PC编程的文章,并试图将它们应用到20世纪90年代的8位传统架构上。错误的工具,错误的目的,错误的系统。

volatile不会在任何系统(包括AVR)上实现代码可重入/线程安全/中断安全。
volatile在此上下文中的用途是当编译器没有意识到ISR是由硬件而不是由程序调用时,防止不正确的编译器优化。我们不能用volatile限定C中的函数或代码,只能限定对象,因此与ISR共享的变量需要用volatile限定。详细信息和示例如下:https://electronics.stackexchange.com/questions/409545/using-volatile-in-embedded-c-development/409570#409570
至于你应该做些什么,我相信我对你另一个问题的回答已经涵盖了这一点。

lb3vh1jj

lb3vh1jj2#

__asm__ __volatile__ ("" :: "m" (flag):"memory"); // <2>
由于"memory",您正在占用所有内存。
如果您想表示只有flag发生了更改(并且更改具有volatile效应),则:
__asm__ __volatile__ ("" : "+m" (flag));
这告诉GCC flag被更改了,而不仅仅是像中那样的asm输入<2>。

hjzp0vay

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优化它的启发式算法只考虑了几个调用的链,而不去证明没有价值相关的东西发生

#include <stdint.h>

//uint8_t flag;
static uint8_t g;

void func(void) {
  __asm__ __volatile__ ("" ::: "memory"); // <1>
    
    int tmp = g;
    g = tmp;
    ++g; g = 1;
    // g = tmp+1;  // without a later constant store to make it dead will make GCC miss the optimization

  __asm__ __volatile__ ("" ::: "memory");  // <1>

}

Godbolt上AVR的GCC 12-O3输出:

func:
        ret

"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。)

相关问题