(问题最初是由这个答案中的评论引起的.这个生产者 - 消费者实现中是否存在竞争条件?但是这里严格地从C语言角度提出这个问题,没有涉及任何并发或多线程.)
考虑这个最小的代码:
#define BUFSIZ 10
char buf[BUFSIZ];
void f(int *pn)
{
buf[*pn]++;
*pn = (*pn + 1) % BUFSIZ;
}
int main()
{
int n = 0;
f(&n);
return n;
}
Run Code Online (Sandbox Code Playgroud)
问题:C "as-if"规则是否允许编译器重写代码如下?
void f(int *pn)
{
int n = *pn;
*pn = (*pn + 1) % BUFSIZ;
buf[n]++;
}
Run Code Online (Sandbox Code Playgroud)
一方面,上述内容不会改变程序的可观察行为.
另一方面,f
可能使用无效索引调用,可能来自另一个翻译单元:
int g()
{
int n = -1001;
f(&n);
}
Run Code Online (Sandbox Code Playgroud)
在后一种情况下,代码的两种变体在访问越界数组元素时都会调用UB.但是,原始代码将保留*pn
传入的值f
(= -1001),而重写的代码只有在修改*pn
(to 0
)后才会进入UB-land .
这样的差异是否会被视为"可观察",或者回到实际问题,C标准中是否有任何内容可以特别允许或排除这种类型的代码重写/优化?
如果程序的任何部分具有未定义的行为,则整个程序的行为未定义.换句话说,即使在"行为未定义"的任何构造"之前",程序的行为也是未定义的.(这是允许编译器执行某些优化所必需的,这些优化取决于所定义的行为.)
鉴于这两个变量都没有声明为volatile,我相信内存更新的顺序可能会按照指示进行重新排序,因为只有在没有未定义的行为的情况下才能保证可观察行为符合执行模型.
"可观察行为"(标准C中)在§5.1.2.3中定义为:
- 根据抽象机器的规则严格评估对易失性对象的访问.
- 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序的结果相同.
- 交互设备的输入和输出动态应按照7.21.3的规定进行.这些要求的目的是尽快出现无缓冲或行缓冲输出,以确保在程序等待输入之前实际出现提示消息.
此列表不包括对未定义行为(例如陷阱或信号)的任何潜在响应,即使用本地术语表示段错误通常是可观察的.问题中的特定示例不涉及这三点中的任何一点.(UB可以阻止程序成功终止,这基本上使可观察行为中的第二点无效.)因此,在问题中代码的特定情况下,重新排序不会改变任何可观察的行为并且可以清楚地执行.
我的声明,即实现对未定义行为的响应不仅限于严格执行导致未定义行为的组件,而是在注释线程中引起了比我预期更多的争议,因为它是现代C的一个相当着名的特性.它可能值得回顾John Regehr 关于未定义行为的有用论文,我引用它:(在第三部分)
更具体地说,当程序因执行非法操作(例如除以零或取消引用空指针)而死亡时,这被认为是副作用吗?答案绝对是"不"....由于崩溃诱导操作不会产生副作用,编译器可以根据其他操作对它们进行重新排序,
作为一个可能更有趣的例子(取自评论线程),如果一个程序产生几行输出,然后故意执行一个显式的被零除零,那么人们可能期望编译和运行程序会在响应之前产生输出无论以何种未定义的方式,它都会响应零除.然而,检测到被零除的并且可以证明程序的控制流保证其执行的编译器将完全有权在转换时产生错误消息并且拒绝产生可执行映像.
或者,如果它不能证明控制流达到零除,它就有权假设零除零不可能发生,因此删除所有代码,明确地导致除零(包括对输出函数的调用)作为死代码.
上述两个内容都符合§3.4.3中未定义行为的示例响应列表:"完全忽略不可预测的结果,...终止翻译或执行(发出诊断消息)".