原始 new[]/delete[] 与 std::vector 的优化

Optimization of raw new[]/delete[] vs std::vector

让我们来研究一下非常基本的动态分配内存。我们取一个向量 3,设置它的元素和 return 向量的总和。

在第一个测试用例中,我使用了带有 new[]/delete[] 的原始指针。在第二个我使用 std::vector:

#include <vector>   

int main()
{
  //int *v = new int[3];        // (1)
  auto v = std::vector<int>(3); // (2)


  for (int i = 0; i < 3; ++i)
    v[i] = i + 1;

  int s = 0;
  for (int i = 0; i < 3; ++i)
    s += v[i];

  //delete[] v;                 // (1)
  return s;
}

(1) 的组装 (new[]/delete[])

main:                                   # @main
        mov     eax, 6
        ret

(2) 的组装 (std::vector)

main:                                   # @main
        push    rax
        mov     edi, 12
        call    operator new(unsigned long)
        mov     qword ptr [rax], 0
        movabs  rcx, 8589934593
        mov     qword ptr [rax], rcx
        mov     dword ptr [rax + 8], 3
        test    rax, rax
        je      .LBB0_2
        mov     rdi, rax
        call    operator delete(void*)
.LBB0_2:                                # %std::vector<int, std::allocator<int> >::~vector() [clone .exit]
        mov     eax, 6
        pop     rdx
        ret

两个输出都来自 https://gcc.godbolt.org/-std=c++14 -O3

在两个版本中,returned 值是在编译时计算的,因此我们只看到 mov eax, 6; ret.

对于原始 new[]/delete[],动态分配已被完全删除。然而,使用std::vector,内存被分配、设置和释放。

即使使用未使用的变量 auto v = std::vector<int>(3)也会发生这种情况:调用new,设置内存,然后调用delete

我意识到这很可能是一个几乎不可能给出的答案,但也许有人有一些见解并且可能会弹出一些有趣的答案。

std::vector 情况下,不允许编译器优化删除内存分配的影响因素是什么,就像在原始内存分配情况下一样?

当使用指向动态分配数组的指针时(直接使用 new[] 和 delete[]),编译器优化了对 operator newoperator delete 的调用,即使它们有可观察的一面效果。 C++ 标准第 5.3.4 节第 10 段允许此优化:

An implementation is allowed to omit a call to a replaceable global allocation function (18.6.1.1, 18.6.1.2). When it does so, the storage is instead provided by the implementation or...

我会在最后显示剩余的句子,这很重要。

这个优化相对较新,因为它首先在 C++14 中被允许(提案 N3664). Clang supported it since 3.4。最新版本的 gcc,即 5.3.0,没有利用 as-if规则。它产生以下代码:

main:
        sub     rsp, 8
        mov     edi, 12
        call    operator new[](unsigned long)
        mov     DWORD PTR [rax], 1
        mov     DWORD PTR [rax+4], 2
        mov     rdi, rax
        mov     DWORD PTR [rax+8], 3
        call    operator delete[](void*)
        mov     eax, 6
        add     rsp, 8
        ret

MSVC 2013 也不支持此优化。它产生以下代码:

main:
  sub         rsp,28h  
  mov         ecx,0Ch  
  call        operator new[] ()  
  mov         rcx,rax  
  mov         dword ptr [rax],1  
  mov         dword ptr [rax+4],2  
  mov         dword ptr [rax+8],3  
  call        operator delete[] ()  
  mov         eax,6  
  add         rsp,28h  
  ret 

我目前无法访问 MSVC 2015 Update 1,因此我不知道它是否支持此优化。

最后是icc 13.0.1生成的汇编代码:

main:
        push      rbp                                          
        mov       rbp, rsp                                   
        and       rsp, -128                                    
        sub       rsp, 128                                     
        mov       edi, 3                                       
        call      __intel_new_proc_init                         
        stmxcsr   DWORD PTR [rsp]                               
        mov       edi, 12                                 
        or        DWORD PTR [rsp], 32832                       
        ldmxcsr   DWORD PTR [rsp]                               
        call      operator new[](unsigned long)
        mov       rdi, rax                                      
        mov       DWORD PTR [rax], 1                            
        mov       DWORD PTR [4+rax], 2                          
        mov       DWORD PTR [8+rax], 3                         
        call      operator delete[](void*)
        mov       eax, 6    
        mov       rsp, rbp                           
        pop       rbp                                   
        ret                                          

很明显,它不支持这个优化。我无法访问最新版本的 icc,即 16.0.

所有这些代码片段都是在启用优化的情况下生成的。

使用 std::vector 时,所有这些编译器都没有优化分配。当编译器不执行优化时,要么是因为某些原因它不能执行,要么就是还不受支持。

What are the contributing factors that don't allow compiler optimizations to remove the memory allocation in the std::vector case, like in the raw memory allocation case?

编译器没有执行优化,因为不允许这样做。要看这个,我们来看5.3.4第10段剩下的句子:

An implementation is allowed to omit a call to a replaceable global allocation function (18.6.1.1, 18.6.1.2). When it does so, the storage is instead provided by the implementation or provided by extending the allocation of another new-expression.

这就是说,只有当它源自 new-expression 时,您才可以忽略对可替换全局分配函数的调用。 Anew-expression同节第1段定义

下面的表达式

new int[3]

是一个 new-expression,因此允许编译器优化关联的分配函数调用。

另一方面,以下表达式:

::operator new(12)

不是 new-expression(参见 5.3.4 第 1 段)。这只是一个函数调用表达式。换句话说,这被视为典型的函数调用。此函数无法优化掉,因为它是从另一个共享库导入的(即使您静态链接运行时,函数本身也会调用另一个导入的函数)。

std::vector 使用的默认分配器使用 ::operator new 分配内存,因此不允许编译器优化它。

我们来测试一下。这是代码:

int main()
{
  int *v =  (int*)::operator new(12);

  for (int i = 0; i < 3; ++i)
    v[i] = i + 1;

  int s = 0;
  for (int i = 0; i < 3; ++i)
    s += v[i];

  delete v;
  return s;
}

使用Clang 3.7编译得到如下汇编代码:

main:                                   # @main
        push    rax
        mov     edi, 12
        call    operator new(unsigned long)
        movabs  rcx, 8589934593
        mov     qword ptr [rax], rcx
        mov     dword ptr [rax + 8], 3
        test    rax, rax
        je      .LBB0_2
        mov     rdi, rax
        call    operator delete(void*)
.LBB0_2:
        mov     eax, 6
        pop     rdx
        ret

这与使用 std::vector 时生成的汇编代码完全相同,除了 mov qword ptr [rax], 0 来自 std::vector 的构造函数(编译器应该删除它但没有这样做所以因为它的优化算法存在缺陷)。