我在几个嵌入式平台的编译器上工作。用户最近抱怨我们的一个编译器出现以下行为。给出这样的代码:
extern volatile int MY_REGISTER;
void Test(void)
{
(void) (MY_REGISTER = 1);
}
编译器生成它(在伪汇编程序中):
Test:
move regA, 1
store regA, MY_REGISTER
load regB, MY_REGISER
也就是说,它不仅写入MY_REGISTER,而且之后将其读回。由于性能原因,额外负载使他心烦意乱。我解释说这是因为根据标准“赋值表达式在赋值后具有左操作数的值,[...]”。
奇怪的是,删除cast-to-void会改变行为:负载消失。用户很高兴,但我只是感到困惑。
所以我也在几个版本的GCC(3.3和4.4)中检查了这一点。在那里,编译器永远不会生成负载,即使明确使用了该值,例如
int TestTwo(void)
{
return (MY_REGISTER = 1);
}
变成
TestTwo:
move regA, 1
store regA, MY_REGISTER
move returnValue, 1
return
有没有人对该标准的正确解释有何看法?回读是否应该发生?如果使用该值或将其转换为void,则添加只读是正确还是有用?
答案 0 :(得分:7)
标准中的相关段落是
赋值运算符存储值 在左边指定的对象中 操作数。赋值表达式有 之后左操作数的值 赋值,但不是左值。 赋值表达式的类型 是除左边操作数的类型 左操作数具有限定类型, 在这种情况下,它是不合格的 左侧类型的版本 操作数。更新左操作数的存储值的副作用应为 发生在前一个和下一个序列点之间。
因此,这明显区分了“左操作数的值”和存储值的更新。另请注意,返回值不是左值(因此在表达式的返回中没有对变量的引用)并且所有限定符都将丢失。
所以我读到这是因为当gcc返回它知道必须存储的值时,正在做正确的事。
修改强>
即将推出的标准计划通过添加脚注来澄清:
允许实施 读取对象以确定值 但不是必须的,即使是 object具有volatile限定类型。
编辑2:
实际上还有一个关于表达式陈述的段落可能会对此有所了解:
表达式中的表达式 声明被评估为无效 表达其副作用。\ footnote {例如赋值和具有副作用的函数调用}
由于这意味着对于这样的语句不需要返回值的效果,这强烈暗示如果使用该值,则只能从变量加载该值。
总而言之,当客户看到变量已加载时,他的确非常不满。如果你扩展它的解释,这种行为可能符合标准,但它显然是在可接受的边界线上。
答案 1 :(得分:6)
回读似乎更接近标准(特别是考虑到读取一个volatile变量可能导致与写入的值不同的值),但我很确定它不是大多数使用volatile的代码所期望的特别是在读取或写入volatile变量触发其他一些影响的情况下。
volatile一般来说定义不明确 - “什么构成了对象的访问权限 volatile的限定类型是实现定义的。“
编辑:如果我必须编写一个编译器,我想我不会读回变量,如果没有使用它并重新读取它,如果是,但带有警告。那么应该使用无效的演员吗?
(void) v;
肯定应该是一个,考虑到这一点,我没有任何理由
(void) v = exp;
不是。但无论如何,我会发出警告,解释如何获得其他效果。
顺便说一句,如果您在编译器上工作,您可能有人与C委员会联系,填写正式的缺陷报告将为您带来一个具有约束力的解释(好吧,存在DR被归类为“非缺陷”的风险) “没有暗示他们想要什么......”答案 2 :(得分:1)
标准中的语言没有说明读取volatile变量,只是赋值表达式的值是什么,a)是由C语义定义的,而不是由变量的内容定义的,b)这里不使用,所以无需计算。