> 文章列表 > 编译器的过度优化

编译器的过度优化

编译器的过度优化

前言

编译器在进行优化的时候,可能为了效率而交换不相关的两条相邻指令的执行顺序。也就是指令重排,这也就引发了一些问题,下面就带大家看两个经典的问题。

单例模式

第一个例子来自单例模式的双加锁,下面是典型的双加锁的单例模式代码:

T* ptr = nullptr;T* GetInstance() {if (nullptr == ptr) {lock();if (nullptr == ptr) {ptr = new T;}unlock();}return ptr;
}

上面的代码看起来没问题,并且采用了双加锁,能减少锁的竞争。

我们知道 C++ 的 new 做了两件事:

  1. 调用 ::operator new 分配内存
  2. 调用构造函数

所以 ptr = new T 包含了三个步骤:

  1. 调用 ::operator new 分配内存
  2. 在内存的位置上调用构造函数
  3. 将内存的地址赋值给 ptr

在这三步中,2 和 3 的顺序是可以交换的。也就是说,有可能:有一个线程分配了内存并将地址赋值给 ptr 了,但还没有初始化该内存。另一线程检测 ptr 不为空,就直接拿去使用了,这时可能引起不可预料的结果。

通常情况下,可以调用 CPU 提供的一条指令来解决该情况,这指令被称为 barrier。一条 barrier 指令会阻止 CPU 将该指令交换到 barrier 之后,也不能将之后的指令交换到 barrier 之前。

#define barrier() __asm__ volaticle("lwsync") 
T* ptr = nullptr;T* GetInstance() {if (nullptr == ptr) {lock();if (nullptr == ptr) {T* tmp = new T;barrier();ptr = tmp;}unlock();}return ptr;
}

智能指针

有时我们会采用智能指针来管理内存,防止我们忘记释放或在某些场景手动释放十分麻烦。

我们有如下代码:

processQgw(shared_ptr<Qgw>(new Qgw), test());

令人遗憾的是,上述代码仍可能产生内存泄漏,并难以察觉。

编译器在调用函数前,需要准备好参数,所以上面代码会做以下三件事:

  • 调用 test
  • 执行 new Qgw
  • 调用 shared_ptr 构造函数

C++ 编译器会以什么次序完成这些事情呢?可以确定的是 new Qgw 一定在 调用 shared_ptr 之前完成,上述三件事也一定在调用 processQgw 之前完成。编译器可能以下列次序调用:

  1. 执行 new Qgw
  2. 调用 test
  3. 调用 shared_ptr 构造函数

如果 test 的调用导致异常,new Qgw 返回的指针就再也找不到了,也就引起了内存泄漏。因为我们还没将返回的指针置入智能指针,智能指针也就对这种情况无能为力了。

为避免这类问题:使用分离语句,分别写出创建 Qgw,将它置入一个智能指针内,然后再把智能指针传给 processQgw:

shared_ptr<Qgw> pq = new Qgw;
processQgw(pq, test());

上述解决方法之所以能行,是因为编译器对于「跨越语句的各项操作」没有重新排列的自由,只有在一条语句内它才拥有那个自由。