C++ large deque - 程序需要很长时间才能退出?

C++ large deque - program takes very long time to exit?

考虑以下 C++ 程序:

#include <deque>
#include <iostream>

using namespace std;

int main()
{
    deque<double> d(30000000);
    cout << "Done\n";
}

第一行的内存分配只需要一秒钟,但在它打印 Done 后,需要 33 秒(!)退出回到终端。将元素数量减少到 20000000 会将时间减少到 22 秒,因此很明显它与元素数量呈线性关系。

我在 Windows 10 上编译,同样的事情发生在 GCC 10.2.0 和 Visual Studio 2019 上。

这是怎么回事?我是否以不应该使用的方式使用 deque

编辑:

#include <deque>
#include <iostream>

using namespace std;

void test_deque()
{
    deque<double> d(30000000);
    cout << "Function done\n";
}

int main()
{
    test_deque();
    cout << "Main done\n";
}

现在它打印 Function done 然后有 33 秒的延迟。所以我认为这与函数退出时执行的析构函数有关。但是为什么销毁240MB的内存需要这么长时间?

编辑 2:在 Ubuntu 上使用 GCC 进行了尝试(第二个版本),运行 只需几分之一秒!与一些在线 C++ 编译器相同。这是 Windows 特有的问题吗?

编辑 3:对于 vector,运行 也需要几分之一秒。但是,对于 list(和 forward_list),我得到了类似的极长延迟。

编辑 4:在 Release(而不是 Debug)配置中使用 MSVC 编译也需要几分之一秒。我不确定 GCC 等同于什么,但是使用 -O3(最大优化)执行时间仍然是 33 秒。

基本上答案不是很有趣。您的程序是 no-op,因此编译器 可能 优化双端队列构造。但它不必。

但首先,合法的理智实施可能会执行以下任一操作:

  1. 只分配 30000000 个浮点元素。分配器可能:

    1. 以最懒惰的方式进行分配,除了记账之外什么都不做。
    2. 急于在内存中分配和分页,导致 30000000/页大小操作。
    3. Zero-initialize 或 pattern-initialize(例如 0xdeadbeef)以帮助检测未初始化的内存使用,导致 30000000 次写入。
  2. 分配(包括上面)和zero-initialize或pattern-initialize内存。

  3. 运行 某种对所有元素的析构函数(例如清零内存)。

  4. 不是任何元素的 运行 析构函数,因为它是 built-in 类型。

现在以上都是可能的选择。由于您的程序是 no-op,合法的编译器可能会优化这些步骤中的任何或 none。您的系统分配器可能在功能上有所不同,支持惰性分配、过度使用、自动归零等。因此最终结果是您可以获得任何类型的行为,具体取决于您的操作系统、编译器版本、编译器标志、标准库等。

在调试模式下真的很慢。

MSDN:

调试器创建的进程(也称为派生进程)的行为与调试器未创建的进程略有不同。

调试器创建的进程不使用标准堆 API,而是使用特殊的调试堆。您可以使用 _NO_DEBUG_HEAP 环境变量强制生成的进程使用标准堆而不是调试堆。

std::deque 以固定大小的块分配数据,随平台和类型的不同而不同。对于 double 可能是 4KB。因此,分配 30,000,000 个双精度数需要 2.4GB 内存,因此需要 6,000 allocations/deallocations。使用 std::list 那将是 30,000,000 allocations/deallocations 并占用几 GB 的更多内存使它变得非常慢。

这甚至可能导致内存碎片问题,具体取决于您的硬件。如果你 运行 没有优化,它会更慢。

还有隐私问题。解除分配可能会清除数据以确保您的程序不会向外部程序泄露任何信息。

正如@orlp 所提到的,因为您的程序是 no-op,整个 allocation/deallocation 可以完全优化,这也许可以解释为什么它有时会显着加速。

MSVC 有一个内置的分析器。我们可以 运行 它(按 Alt-F2)可以看到大部分 CPU 时间花在构造函数和析构函数中,它们分别调用 deque::resize()deque::_Tidy() 函数.

如果我们进一步深入,我们会发现 deque::emplace_back() 产生了相当多的代码

#define _PUSH_BACK_BEGIN                                                                                \
    if ((_Myoff() + _Mysize()) % _DEQUESIZ == 0 && _Mapsize() <= (_Mysize() + _DEQUESIZ) / _DEQUESIZ) { \
        _Growmap(1);                                                                                    \
    }                                                                                                   \
    _Myoff() &= _Mapsize() * _DEQUESIZ - 1;                                                             \
    size_type _Newoff = _Myoff() + _Mysize();                                                           \
    size_type _Block  = _Getblock(_Newoff);                                                             \
    if (_Map()[_Block] == nullptr) {                                                                    \
        _Map()[_Block] = _Getal().allocate(_DEQUESIZ);                                                  \
    }

#define _PUSH_BACK_END ++_Mysize()

    template <class... _Valty>
    decltype(auto) emplace_back(_Valty&&... _Val) {
        _Orphan_all();
        _PUSH_BACK_BEGIN;
        _Alty_traits::construct(
            _Getal(), _Unfancy(_Map()[_Block] + _Newoff % _DEQUESIZ), _STD forward<_Valty>(_Val)...);
        _PUSH_BACK_END;

#if _HAS_CXX17
        return back();
#endif // _HAS_CXX17
    }

反汇编视图:

    template <class... _Valty>
    decltype(auto) emplace_back(_Valty&&... _Val) {
00007FF674A238E0  mov         qword ptr [rsp+8],rcx  
00007FF674A238E5  push        rbp  
00007FF674A238E6  push        rdi  
00007FF674A238E7  sub         rsp,138h  
00007FF674A238EE  lea         rbp,[rsp+20h]  
00007FF674A238F3  mov         rdi,rsp  
00007FF674A238F6  mov         ecx,4Eh  
00007FF674A238FB  mov         eax,0CCCCCCCCh  
00007FF674A23900  rep stos    dword ptr [rdi]  
00007FF674A23902  mov         rcx,qword ptr [rsp+158h]  
00007FF674A2390A  lea         rcx,[__0657B1E2_deque (07FF674A3E02Fh)]  
00007FF674A23911  call        __CheckForDebuggerJustMyCode (07FF674A21159h)  
        _Orphan_all();
00007FF674A23916  mov         rcx,qword ptr [this]  
00007FF674A2391D  call        std::deque<double,std::allocator<double> >::_Orphan_all (07FF674A217FDh)  
        _PUSH_BACK_BEGIN;
00007FF674A23922  mov         rcx,qword ptr [this]  
00007FF674A23929  call        std::deque<double,std::allocator<double> >::_Myoff (07FF674A2139Dh)  
00007FF674A2392E  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23935  mov         rcx,qword ptr [this]  
00007FF674A2393C  call        std::deque<double,std::allocator<double> >::_Mysize (07FF674A211B8h)  
00007FF674A23941  mov         rcx,qword ptr [rbp+0F8h]  
00007FF674A23948  mov         rcx,qword ptr [rcx]  
00007FF674A2394B  add         rcx,qword ptr [rax]  
00007FF674A2394E  mov         rax,rcx  
00007FF674A23951  xor         edx,edx  
00007FF674A23953  mov         ecx,2  
00007FF674A23958  div         rax,rcx  
00007FF674A2395B  mov         rax,rdx  
00007FF674A2395E  test        rax,rax  
00007FF674A23961  jne         std::deque<double,std::allocator<double> >::emplace_back<>+0D0h (07FF674A239B0h)  
00007FF674A23963  mov         rcx,qword ptr [this]  
00007FF674A2396A  call        std::deque<double,std::allocator<double> >::_Mapsize (07FF674A214BFh)  
00007FF674A2396F  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23976  mov         rcx,qword ptr [this]  
00007FF674A2397D  call        std::deque<double,std::allocator<double> >::_Mysize (07FF674A211B8h)  
00007FF674A23982  mov         rax,qword ptr [rax]  
00007FF674A23985  add         rax,2  
00007FF674A23989  xor         edx,edx  
00007FF674A2398B  mov         ecx,2  
00007FF674A23990  div         rax,rcx  
00007FF674A23993  mov         rcx,qword ptr [rbp+0F8h]  
00007FF674A2399A  cmp         qword ptr [rcx],rax  
00007FF674A2399D  ja          std::deque<double,std::allocator<double> >::emplace_back<>+0D0h (07FF674A239B0h)  
00007FF674A2399F  mov         edx,1  
00007FF674A239A4  mov         rcx,qword ptr [this]  
00007FF674A239AB  call        std::deque<double,std::allocator<double> >::_Growmap (07FF674A21640h)  
00007FF674A239B0  mov         rcx,qword ptr [this]  
00007FF674A239B7  call        std::deque<double,std::allocator<double> >::_Mapsize (07FF674A214BFh)  
00007FF674A239BC  mov         rax,qword ptr [rax]  
00007FF674A239BF  lea         rax,[rax+rax-1]  
00007FF674A239C4  mov         qword ptr [rbp+0F8h],rax  
00007FF674A239CB  mov         rcx,qword ptr [this]  
00007FF674A239D2  call        std::deque<double,std::allocator<double> >::_Myoff (07FF674A2139Dh)  
00007FF674A239D7  mov         qword ptr [rbp+100h],rax  
00007FF674A239DE  mov         rax,qword ptr [rbp+100h]  
00007FF674A239E5  mov         rax,qword ptr [rax]  
00007FF674A239E8  mov         qword ptr [rbp+108h],rax  
00007FF674A239EF  mov         rax,qword ptr [rbp+0F8h]  
00007FF674A239F6  mov         rcx,qword ptr [rbp+108h]  
00007FF674A239FD  and         rcx,rax  
00007FF674A23A00  mov         rax,rcx  
00007FF674A23A03  mov         rcx,qword ptr [rbp+100h]  
00007FF674A23A0A  mov         qword ptr [rcx],rax  
00007FF674A23A0D  mov         rcx,qword ptr [this]  
00007FF674A23A14  call        std::deque<double,std::allocator<double> >::_Myoff (07FF674A2139Dh)  
00007FF674A23A19  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23A20  mov         rcx,qword ptr [this]  
00007FF674A23A27  call        std::deque<double,std::allocator<double> >::_Mysize (07FF674A211B8h)  
00007FF674A23A2C  mov         rcx,qword ptr [rbp+0F8h]  
00007FF674A23A33  mov         rcx,qword ptr [rcx]  
00007FF674A23A36  add         rcx,qword ptr [rax]  
00007FF674A23A39  mov         rax,rcx  
00007FF674A23A3C  mov         qword ptr [_Newoff],rax  
00007FF674A23A40  mov         rdx,qword ptr [_Newoff]  
00007FF674A23A44  mov         rcx,qword ptr [this]  
00007FF674A23A4B  call        std::deque<double,std::allocator<double> >::_Getblock (07FF674A21334h)  
00007FF674A23A50  mov         qword ptr [_Block],rax  
00007FF674A23A54  mov         rcx,qword ptr [this]  
00007FF674A23A5B  call        std::deque<double,std::allocator<double> >::_Map (07FF674A21753h)  
00007FF674A23A60  mov         rax,qword ptr [rax]  
00007FF674A23A63  mov         rcx,qword ptr [_Block]  
00007FF674A23A67  cmp         qword ptr [rax+rcx*8],0  
00007FF674A23A6C  jne         std::deque<double,std::allocator<double> >::emplace_back<>+1D7h (07FF674A23AB7h)  
00007FF674A23A6E  mov         rcx,qword ptr [this]  
00007FF674A23A75  call        std::deque<double,std::allocator<double> >::_Getal (07FF674A216CCh)  
00007FF674A23A7A  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23A81  mov         edx,2  
00007FF674A23A86  mov         rcx,qword ptr [rbp+0F8h]  
00007FF674A23A8D  call        std::allocator<double>::allocate (07FF674A216C7h)  
00007FF674A23A92  mov         qword ptr [rbp+100h],rax  
00007FF674A23A99  mov         rcx,qword ptr [this]  
00007FF674A23AA0  call        std::deque<double,std::allocator<double> >::_Map (07FF674A21753h)  
00007FF674A23AA5  mov         rax,qword ptr [rax]  
00007FF674A23AA8  mov         rcx,qword ptr [_Block]  
00007FF674A23AAC  mov         rdx,qword ptr [rbp+100h]  
00007FF674A23AB3  mov         qword ptr [rax+rcx*8],rdx  
        _Alty_traits::construct(
00007FF674A23AB7  mov         rcx,qword ptr [this]  
00007FF674A23ABE  call        std::deque<double,std::allocator<double> >::_Map (07FF674A21753h)  
00007FF674A23AC3  mov         rax,qword ptr [rax]  
00007FF674A23AC6  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23ACD  xor         edx,edx  
00007FF674A23ACF  mov         rax,qword ptr [_Newoff]  
00007FF674A23AD3  mov         ecx,2  
00007FF674A23AD8  div         rax,rcx  
00007FF674A23ADB  mov         rax,rdx  
00007FF674A23ADE  mov         rcx,qword ptr [_Block]  
00007FF674A23AE2  mov         rdx,qword ptr [rbp+0F8h]  
00007FF674A23AE9  mov         rcx,qword ptr [rdx+rcx*8]  
00007FF674A23AED  lea         rax,[rcx+rax*8]  
00007FF674A23AF1  mov         rcx,rax  
00007FF674A23AF4  call        std::_Unfancy<double> (07FF674A214A6h)  
00007FF674A23AF9  mov         qword ptr [rbp+100h],rax  
00007FF674A23B00  mov         rcx,qword ptr [this]  
00007FF674A23B07  call        std::deque<double,std::allocator<double> >::_Getal (07FF674A216CCh)  
00007FF674A23B0C  mov         qword ptr [rbp+108h],rax  
00007FF674A23B13  mov         rdx,qword ptr [rbp+100h]  
00007FF674A23B1A  mov         rcx,qword ptr [rbp+108h]  
00007FF674A23B21  call        std::_Default_allocator_traits<std::allocator<double> >::construct<double> (07FF674A211E5h)  
            _Getal(), _Unfancy(_Map()[_Block] + _Newoff % _DEQUESIZ), _STD forward<_Valty>(_Val)...);
        _PUSH_BACK_END;
00007FF674A23B26  mov         rcx,qword ptr [this]  
00007FF674A23B2D  call        std::deque<double,std::allocator<double> >::_Mysize (07FF674A211B8h)  
00007FF674A23B32  mov         qword ptr [rbp+0F8h],rax  
00007FF674A23B39  mov         rax,qword ptr [rbp+0F8h]  
00007FF674A23B40  mov         rax,qword ptr [rax]  
00007FF674A23B43  inc         rax  
00007FF674A23B46  mov         rcx,qword ptr [rbp+0F8h]  
00007FF674A23B4D  mov         qword ptr [rcx],rax  

#if _HAS_CXX17
        return back();
00007FF674A23B50  mov         rcx,qword ptr [this]  
00007FF674A23B57  call        std::deque<double,std::allocator<double> >::back (07FF674A2127Bh)  
#endif // _HAS_CXX17
    }
00007FF674A23B5C  lea         rsp,[rbp+118h]  
00007FF674A23B63  pop         rdi  
00007FF674A23B64  pop         rbp  
00007FF674A23B65  ret

显然 std::deque 没有预先分配元素,而是使用循环一个一个地添加它们。所以难怪它很慢。

您可以通过启用一些优化(例如 /Ob1)和减少 运行 时间检查(例如删除 /RTC1)来加快调试构建。

但实际上,从性能的角度来看,std::deque 只是一个糟糕的结构(一个微小向量的向量 - 根本不适合缓存)。