在此示例中:
volatile uint32_t * pOne = 0xDEADBEEF;
volatile uint32_t * pTwo = 0x0BADC0DE;
void same(void)
{
uint32_t tmp;
tmp = *pOne; // A
*pOne = 0; // B
*pOne = tmp; // C
}
void different(void)
{
uint32_t tmp;
tmp = *pOne;
*pOne = 0; // E
*pTwo = 0; // F
*pOne = tmp;
}
据我所知,C99编译器不允许对函数A
中的行B
,C
和same()
重新排序,因为它们都引用了相同的易失性对象。
但是函数E
中的行F
和different()
呢?它们与不同的易失性对象进行交互。
E
和F
行进行重新排序?我无法在标准本身中找到答案,因为第5.1.2.3节对我来说有些混乱。因此,如果您能解释一下,我将很高兴。
我知道这仅涉及编译器的重新排序,而不会影响处理器的任何重新排序。
那么有没有一个标准的库,(如果实现了)它提供了内存屏障?
此刻,我坚持使用C99,但出于好奇:C11是否有任何变化?
答案 0 :(得分:0)
在volatile
对象的语义方面,实现可以有很大的自由度。程序的可观察行为必须与以指定顺序执行的所有易失对象上的所有操作一致,但是实现具有一揽子许可,可以执行几乎不影响程序可观察行为的所有操作。在“实现定义的行为”领域内,实现还具有相当广泛的权限来指定volatile限定的访问的哪些方面是“可观察的”,哪些不是“可观察的”。
一个不适合嵌入式编程的一致性实现可以指定符合volatile
的访问将以怪异和任意的方式表现出来,从而使其无法用于此类目的。在大多数平台上,应该相当清楚地表明,旨在允许不使用编译器特定指令的嵌入式编程的高质量实现应如何处理volatile
限定的访问,而打算用于此类使用的高质量实现应如何对其进行处理即使标准不要求他们这样做,也应以这种方式进行。不幸的是,一些编译器作者将其语义限制为标准明确要求的语义,而不是扩展其语义以匹配基础平台(例如,当针对volatile
访问可能表现为子例程调用的平台时,请勿对任何操作进行重新排序)除非可以在对未知函数的调用中对其重新排序,否则将在volatile
访问中进行访问)。尽管icc
似乎以这种方式处理volatile
限定的写入,但是gcc
和clang
之类的编译器如果设计为不适合于启用了优化的嵌入式系统,则会被使用。仅在禁用大多数优化时才这样做。
答案 1 :(得分:0)
根据规格(6.7.3.6)
具有volatile限定类型的对象可以以实现方式未知的方式修改或具有其他未知的副作用。因此,必须严格按照抽象机器的规则评估引用此类对象的任何表达式,如5.1.2.3中所述。此外,在每个序列点上,对象中最后存储的值应与抽象机指定的值一致,除非之前提到的未知因素对其进行了修改。实现定义是对具有volatile限定类型的对象的访问。
在大多数(即使不是全部)实现中,获取/设置该值将被视为“访问”。
从5.1.2.3开始:
访问易失性对象,修改对象,修改文件或调用执行任何这些操作的函数都是副作用,这些副作用是执行环境状态的变化。评价表达可能会产生副作用。在执行序列中某些特定的点(称为顺序点)上,以前评估的所有副作用都应完整,并且以后评估的副作用都不应发生。 (序列点的摘要在附件C中给出。)
在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。
每个分配都是一个序列点,因此,如果实现没有从易失性访问中“推断出没有产生所需的副作用”,那么这意味着这些行不能在实现中重新排序,因为那样就不会相同的语义。
当然,许多编译器都不是100%符合标准的。
在这里您可以看到带有各种编译器的汇编器输出:https://godbolt.org/z/b0TNmT。
GCC,Clang和MSVC都不会对程序集中的读写进行重新排序。 (尽管我不确定这对于实际的可执行文件意味着什么)