C和C++中的编译器优化和临时分配

mrn*_*mrn 10 c c++ embedded concurrency

请参阅以下在C和C++中有效的代码:

extern int output;
extern int input;
extern int error_flag;

void func(void)
{
  if (0 != error_flag)
  {
    output = -1;
  }
  else
  {
    output = input;
  }
}
Run Code Online (Sandbox Code Playgroud)
  1. 是否允许编译器以与下面类似的方式编译上述代码?

    extern int output;
    extern int input;
    extern int error_flag;
    
    void func(void)
    {
      output = -1;
      if (0 == error_flag)
      {
        output = input;
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    换句话说,被编译器允许生成(从第一个片段)的代码,总是让-1到一个临时分配output,然后分配input值,以output根据error_flag状态?

  2. 如果output将被声明为volatile ,编译器是否会被允许这样做?

  3. 如果output声明为atomic_int(stdatomic.h),是否允许编译器执行此操作?

大卫施瓦茨评论后更新:

如果编译器可以自由地向变量添加额外的写入,则似乎无法从C代码中判断是否存在数据争用.怎么判断这个?

ric*_*ici 11

  1. 是的,投机任务是可能的.非易失性变量的修改不是程序的可观察行为的一部分,因此允许虚假写入.(有关"可观察行为"的定义,请参见下文,其中实际上并未包含您可能观察到的所有行为.)

  2. 否.如果outputvolatile,则不允许推测性或虚假突变,因为突变可观察行为的一部分.(写入 - 或从 - 读取 - 硬件寄存器可能会产生除存储值之外的其他后果.这是主要用例之一volatile.)

  3. (编辑)不,推测作业是不可能的atomic output.atomic变量的加载和存储是同步操作,因此不应该加载未明确存储到变量中的此类变量的值.

可观察的行为

尽管程序可以做很多明显观察到的东西(例如,由于段错误的突然终止),C和C++标准只能保证有限的一组结果.可观察到的行为在C11草案§5.1.2.3p6并且在§1.9p8[intro.execution]当前C++ 14草案具有非常类似的措词定义如下:

符合实施的最低要求是:

- 严格根据抽象机的规则来评估对易失性对象的访问.

- 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同.

- 交互设备的输入和输出动态应以在程序等待输入之前提示输出实际传送的方式进行.构成交互设备的是实现定义的.

这些统称为程序的可观察行为.

以上内容取自C++标准; C标准的不同之处在于,在第二点中它不允许多种可能的结果,在第三点中它明确地引用了标准库要求的相关部分.但除了细节,定义是协调的; 对于这个问题的目的,相关的一点是,仅访问易失性的变量是可观察到的(最多到非易失性变量的值被发送到输出装置或文件中的点).

数据竞争

本段还需要在C和C++标准的整体上下文中阅读,如果程序产生未定义的行为,则可以从所有需求中释放实现.这就是为什么在上面的可观察行为的定义中不考虑段错误的原因:段错误是一种可能的未定义行为,但不符合一致性程序中的可能行为.因此,在只有符合要求的程序和符合要求的实现的范围内,没有段错误.

这很重要,因为具有数据竞争的程序符合要求.数据竞争具有未定义的行为,即使它看起来无害.并且由于程序员有责任避免未定义的行为,因此实现可以在不考虑数据争用的情况下进行优化.

C和C++标准中对内存模型的阐述是密集且技术性的,可能不适合作为概念的介绍.(浏览Hans Boehm网站上的材料可能不那么困难.)从标准中提取引用是有风险的,因为细节很重要.但是,从目前的C++ 14标准§1.10[intro.multithread]开始,这里是一个小小的跳跃:

  1. 如果其中一个修改内存位置而另一个读取或修改相同的内存位置,则两个表达式评估会发生冲突.

...

  1. 如果有两个动作可能是并发的

    - 它们由不同的线程执行,或者

    - 它们没有排序,至少有一个是由信号处理程序执行的.

    程序的执行包含数据竞争,如果它包含两个可能同时发生冲突的动作,其中至少有一个不是原子的,并且除了下面描述的信号处理程序的特殊情况之外,它们都不会发生在另一个之前.任何此类数据争用都会导致未定义的行为.

这里的内容是需要同步对同一变量的读取和写入; 否则它是一个数据竞争,结果是未定义的行为.一些程序员可能会反对这种禁令的严格性,认为某些数据竞争是"良性的".这是Hans Boehm 2011年HotPar论文"如何用"良性"数据竞赛"错误编写程序"(pdf)(作者总结:"没有良性数据竞赛")的主题,他解释说这一切都比我能做得更好.

这里的同步包括atomic类型的使用,因此并不是同时读取和修改atomic变量的数据竞争.(读取的结果是不可预测的,但它必须是修改前的值或之后的值.)这可以防止编译器在没有显式同步的情况下对原子变量执行"零碎"修改.

经过一些思考和更多的研究,我的结论是编译器也不能对原子变量进行推测性写入.因此,我修改了问题3的答案,我最初回答"否".

其他有用的参考:

  • Bartosz Milewski:处理Benign数据竞争C++方式

    Milewski处理对原子变量的推测性写入的确切问题,并得出结论:

    编译器是否仍然可以执行相同的脏技巧,并暂时将42存储在owner变量中?不,它不能!由于声明了变量atomic,编译器不能再认为其他线程无法观察到该写入.

  • Herb Sutter关于线程安全和同步

    像往常一样,一个易于理解和写得很好的解释.