在 C/C++ 中创建对象时编译器优化的边界是什么

What's the boundary of compiler optimizations on creation of objects in C/C++

本文关键字:优化 编译器 边界 是什么 创建对象 C++      更新时间:2023-10-16

我是Qt/C++程序员。现在我在研究螺纹安全,我看到了很多关于这方面的讨论。然后我在四个标题下列出了安全问题:

  1. 线程之间读取/写入变量时的原子性
  2. 对线程之间共享的变量的编译器优化
  3. 读取/写入线程之间共享的变量时发出信号中断
  4. 线程之间共享的变量的并发读/写混淆

除了第二条外,我都理解了。我认为理解起来很复杂,因为它并不明确。这取决于编译器的行为,以及我如何预测编译器的行为。例如:

int i=0;
void foo()
{
    for (;i<100;i++)
    {}
{

根据一些资源,在上面的代码中,编译器会将i从内存移动到cpu的寄存器,直到计数结束,然后将其与最终值一起写回。所以,如果在上述代码计数的同时,另一个线程试图读取i的值,会发生什么呢。直到计数结束它才归零。因为在计数结束之前,实际值仍在cpu的寄存器中。因此,将出现意外状态。让我们举一个更进一步的例子:

class MyThread : public QThread
{
    Q_OBJECT
    void run()
    {
         mutex.lock();
         status=true;
         mutex.unlock();
    }
    private:
        QMutex mutex;
        bool status;
};
int main()
{
    MyThread thread;
    thread.start();
    return 0;
}

在上面的代码中,根据Qt文档,thread对象的成员变量归主线程所有,因为它在main()函数中初始化,而run()函数的代码在第二个线程上执行。我使用mutex对象进行访问和原子性的序列化,到目前为止效果很好。但我怎么知道呢,编译器实际上是在第二个线程在run()函数上使用mutex对象之前将其初始化到内存中的。因为编译器永远不会在顺序代码流中看到mutex对象的实际使用,所以它可能不会将其加载到内存中。例如,在编译时,编译器可能会删除一些未在顺序代码流中使用的变量以获得额外的内存,或者可能会在一些内存顺序操作后将所有成员值写入内存。我怎么知道呢?我们如何知道编译器是否优化了变量?

但我怎么知道呢,编译器实际上初始化了互斥在第二个线程在run()函数上使用对象之前,将其放入内存。

如果您是根据第一性原理设计自己的互斥类,那么这确实是一个需要担心的问题。作为优化过程的一部分,编译器不仅可以重新排列代码的顺序,而且即使编译器没有把事情搞砸,现代CPU也经常重新排列它们动态执行指令的顺序,以便更好地利用它们的执行管道。

C++编译器中包含的优化器在"好像规则"下运行,该规则规定,只要生成的程序的可观察行为与您编写的源代码的行为无法区分,它就可以对您的代码进行任何疯狂的转换。对于单线程程序来说,这很好,但一旦你让一个线程尝试在没有任何同步的情况下读取或写入另一个线程的变量,编译器(和CPU)的所有巧妙优化技巧都可能对第二个线程"可见",因此第二个线索很可能会看到未定义的(读作:奇怪和意外)行为,除非你非常小心同步。

那么,像pthreads库(QMutex和QThread类可能在其内部实现中使用)这样的低级线程库的作者是如何隐藏所有这些混乱的呢?他们通过在适当的位置将内存屏障和优化屏障插入到代码中来实现这一点。优化障碍告诉编译器"在优化时不要将访问移动超过这一点,内存障碍是类似的,只是它们由CPU在运行时处理,以限制其无序执行优化。"

由于您使用的是像QMutex这样的高级构造,您不必担心自己指定屏障,因为QMutex和QThread类的成员函数中的代码已经根据需要为您指定了屏障。

但要回答你的问题:默认情况下,编译器优化没有边界,除了程序员或C++规范明确指定的边界(C++规范非常努力地尽可能自由,因为如果不必要的话,它不想排除优化)。