在 C/C++ 中创建 objects 时编译器优化的边界是什么
What's the boundary of compiler optimizations on creation of objects in C/C++
我是 Qt/C++ 程序员。现在我正在研究 thread-safety 并且我看到了很多关于这个的讨论。然后我在四个标题下列出了安全问题:
- 原子性而read/write线程之间的变量
- 编译器优化线程间共享的变量
- 信号中断,而 read/write 变量在线程之间共享
- 线程间共享变量的并发混淆read/write
除了第二条,我都理解了。我认为理解起来非常复杂,因为它不明确。这取决于编译器行为以及我如何预测编译器行为,对吧。例如:
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 Documentation,thread
object的成员变量属于主线程,因为它是在main()
函数中初始化的,而[=的代码18=] 函数在第二个线程上执行。我使用 mutex
object 进行访问序列化和原子性,到目前为止一切顺利。但是我怎么知道,在第二个线程在 run()
函数上使用它之前,编译器实际上将 mutex
object 初始化到内存中。因为编译器永远不会在顺序代码流中看到 mutex
object 的实际用法,所以它可能不会将其加载到内存中。例如在编译时,编译器可能会删除一些不在顺序代码流中使用的变量以获得额外的内存,或者它可能会在一些内存顺序操作后将所有成员值写入内存。我怎么知道?如何知道编译器是否优化了变量?
But how can I know that, the compiler actually initialized mutex
object into memory before second thread used it on run() function.
如果您根据第一原则设计自己的互斥锁 class,这确实是您需要担心的问题。作为优化过程的一部分,编译器不仅可以重新排列代码的顺序,而且即使编译器没有为您搞砸,现代 CPUs 也经常重新排列它们执行指令的顺序- the-fly,以便更好地保持他们的执行管道尽可能得到充分利用。
C++ 编译器中包含的优化器在 "as-if rule" 下运行,它表示它可以根据需要对您的代码进行任何疯狂的转换,只要生成的程序的可观察行为与你写的源代码。对于单线程程序来说这很好,但是一旦你开始让一个线程在没有任何同步的情况下尝试读取或写入另一个线程的变量,所有编译器(和 CPU 的)聪明的优化技巧都会变成 "visible" 到第二个线程,因此第二个线程可能会看到未定义的(阅读:奇怪和意外的)行为,除非你对同步非常小心。
那么像 pthreads 库(QMutex 和 QThread classes 可能在其内部实现中使用的)这样的低级线程库的作者是如何隐藏所有这些混乱的呢?他们通过在代码的适当位置插入 memory barriers and optimization barriers 来做到这一点。优化障碍告诉编译器“在优化时不要将访问移过这一点,并且内存障碍是相似的,除了它们由 CPU 在 运行 时间处理以限制其无序-执行优化。
由于您使用的是像 QMutex 这样的高级构造,因此您不必担心自己指定障碍,因为 QMutex 和 QThread class 的成员函数中的代码已经为您做了这些有必要的。
但要回答你的问题:默认情况下,编译器优化没有界限,除了程序员或 C++ 规范明确指定的那些(并且 C++ 规范非常努力地尽可能自由)可能,因为如果不需要,它不想排除优化。
我是 Qt/C++ 程序员。现在我正在研究 thread-safety 并且我看到了很多关于这个的讨论。然后我在四个标题下列出了安全问题:
- 原子性而read/write线程之间的变量
- 编译器优化线程间共享的变量
- 信号中断,而 read/write 变量在线程之间共享
- 线程间共享变量的并发混淆read/write
除了第二条,我都理解了。我认为理解起来非常复杂,因为它不明确。这取决于编译器行为以及我如何预测编译器行为,对吧。例如:
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 Documentation,thread
object的成员变量属于主线程,因为它是在main()
函数中初始化的,而[=的代码18=] 函数在第二个线程上执行。我使用 mutex
object 进行访问序列化和原子性,到目前为止一切顺利。但是我怎么知道,在第二个线程在 run()
函数上使用它之前,编译器实际上将 mutex
object 初始化到内存中。因为编译器永远不会在顺序代码流中看到 mutex
object 的实际用法,所以它可能不会将其加载到内存中。例如在编译时,编译器可能会删除一些不在顺序代码流中使用的变量以获得额外的内存,或者它可能会在一些内存顺序操作后将所有成员值写入内存。我怎么知道?如何知道编译器是否优化了变量?
But how can I know that, the compiler actually initialized mutex object into memory before second thread used it on run() function.
如果您根据第一原则设计自己的互斥锁 class,这确实是您需要担心的问题。作为优化过程的一部分,编译器不仅可以重新排列代码的顺序,而且即使编译器没有为您搞砸,现代 CPUs 也经常重新排列它们执行指令的顺序- the-fly,以便更好地保持他们的执行管道尽可能得到充分利用。
C++ 编译器中包含的优化器在 "as-if rule" 下运行,它表示它可以根据需要对您的代码进行任何疯狂的转换,只要生成的程序的可观察行为与你写的源代码。对于单线程程序来说这很好,但是一旦你开始让一个线程在没有任何同步的情况下尝试读取或写入另一个线程的变量,所有编译器(和 CPU 的)聪明的优化技巧都会变成 "visible" 到第二个线程,因此第二个线程可能会看到未定义的(阅读:奇怪和意外的)行为,除非你对同步非常小心。
那么像 pthreads 库(QMutex 和 QThread classes 可能在其内部实现中使用的)这样的低级线程库的作者是如何隐藏所有这些混乱的呢?他们通过在代码的适当位置插入 memory barriers and optimization barriers 来做到这一点。优化障碍告诉编译器“在优化时不要将访问移过这一点,并且内存障碍是相似的,除了它们由 CPU 在 运行 时间处理以限制其无序-执行优化。
由于您使用的是像 QMutex 这样的高级构造,因此您不必担心自己指定障碍,因为 QMutex 和 QThread class 的成员函数中的代码已经为您做了这些有必要的。
但要回答你的问题:默认情况下,编译器优化没有界限,除了程序员或 C++ 规范明确指定的那些(并且 C++ 规范非常努力地尽可能自由)可能,因为如果不需要,它不想排除优化。