【问题标题】:C++ large deque - program takes very long time to exit?C++ 大型双端队列 - 程序需要很长时间才能退出?
【发布时间】:2021-02-20 23:43:06
【问题描述】:

考虑以下 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 秒的延迟。所以我认为这与函数退出时执行的析构函数有关。但是为什么销毁 240 MB 内存需要这么长时间呢?

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

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

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

【问题讨论】:

  • 您可以通过将其包装在第二个函数中来进一步调查。这样您就可以看到函数返回与程序退出所需的时间之间的差异。
  • 无法在使用 gcc 10.2.1 的 Linux 上重现。整个过程只需要几分之一秒即可运行。
  • 我也无法使用 Apple clang 11.0.3 进行复制
  • 要尝试的另一件事:其他容器类型——特别是std::vector,它保证是单个连续内存分配(易于释放)。
  • 在 Windows 10 中的 MSVC 上试过这个,该版本将它包装在一个函数中。甚至在打印“功能完成”之前就花了很长时间,然后在打印“完成”之前花了很长时间。从 Debug x64 切换到 Release x64,速度非常快。在 Windows 上具有优化级别的东西?

标签: c++ performance stl deque


【解决方案1】:

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 只是一个可怕的结构(一个微小向量的向量 - 根本不适合缓存)。

【讨论】:

  • 感谢您非常全面的回答!我接受了它,因为它是迄今为止我得到的最佳答案,但我仍然不明白为什么在 Windows 上使用 GCC 编译时这段代码运行如此缓慢,即使进行了最大优化,而使用Linux 上的 GCC 即使没有任何优化,也只需几分之一秒即可运行。我的猜测是,也许 Mingw-w64 端口使用了 MSVC 的双端队列实现,并且不知何故,它以一种不适合任何优化的方式这样做。无论如何,我从中得出的结论是,我根本不应该在我的代码中使用双端队列......
  • Mingw 问题可能是完全不同的问题。我和其他许多人一样,无法重现您对 Mingw 的问题,对我来说,即使没有优化也需要 0.02 秒(我有 gcc 7.1.0 x86_64-posix-seh)。如果您继续遇到 Mingw 问题,那么您可能需要专门发布一个新问题(例如,比较 GCC Mingw 和 Linux 性能)。
  • 我使用的是 gcc 版本 10.2.0(Rev4,由 MSYS2 项目构建)。我相信这可能是在 7.1.0 和 10.2.0 之间的某个时间引入的一个错误......但是,我对使用 MSVC 编译的解决方案感到满意,所以现在我将保留它。谢谢。
【解决方案2】:

std::deque 以固定大小的块分配数据,随平台和类型而变化。对于double,可能是 4KB。因此分配 30,000,000 个 double 需要 2.4GB 内存,因此需要 6,000 个分配/释放。使用std::list,这将是30,000,000 分配/解除分配,并且需要多出几 GB 的内存,这一切都非常缓慢。

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

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

正如@orlp 所提到的,由于您的程序是无操作的,因此可以完全优化整个分配/解除分配,这也许可以解释为什么它有时会显着加速。

【讨论】:

  • 当我在deque 上添加一些操作(所以它不会被优化)时,代码在 MSVC 发布模式下仍然运行不到一秒,在任何其他 MSVC 或 GCC 中运行速度非常慢我试过的模式。此外,根据任务管理器,该程序最多仅使用 305 MB。然后,在 33 秒内,您可以看到该数量正在非常缓慢地减少。销毁这个小内存应该不会花这么长时间,尤其是在 3,600 MHz 的 32 GB DDR4 内存中。我相信这实际上不是预期的行为,并且可能表明存在某种错误。
  • @QuantumField 没有错误。有时操作系统系统在内存映射方面有一些奇怪的技巧。例如,内存可能仅在首次访问后正确分配,并且不会出现在任务管理器中。是的,这是一个计算错误,有一个额外的零。问题在于操作系统内存分配器讨厌的大量小碎片分配。
  • 我发布这个问题的原因是我正在编写一个使用大deque 的程序,我注意到它需要很长时间才能关闭。我正在使用 GCC 编译我的程序,因为我想在 Windows 和 Linux 中使用相同的编译器来编译它。 GCC 优化似乎没有改变任何东西。所以基本上你告诉我我的程序总是需要 33 秒才能关闭,除非我用 MSVC 发布模式而不是 GCC 编译它?这似乎是 GCC(或至少是 Mingw-w64 端口)中的一个错误,而不是预期的行为。
  • @QuantumField 为什么需要大的deque?为什么不简单地使用std::vector?如果您只需要pop_front,您可以使用连续的双端队列。当然,您可以在线找到此类容器的良好实现。
【解决方案3】:

从根本上说,答案不是很有趣。您的程序是无操作的,因此编译器可能优化双端队列结构。但这不是必须的。

但首先,合法、健全的实现可能会执行以下任何操作:

  1. 只分配 30000000 个浮点元素,仅此而已。分配器可能:

    1. 以最懒惰的方式进行分配,基本上只做一些簿记。
    2. 急切地在内存中分配和分页,导致 30000000/页大小的操作。
    3. 零初始化或模式初始化(​​例如0xdeadbeef)以帮助检测未初始化的内存使用情况,导致 30000000 次写入。
  2. 分配(包括以上)和零初始化或模式初始化内存。

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

  4. 不要对任何元素运行析构函数,因为它是内置类型。

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

【讨论】:

  • 正确,但对 OP 没有真正帮助。
  • @PasserBy 为什么不呢? OP问“为什么会发生这种情况”(我尽我所能解释——可能有很多原因)。 OP 没有问“我该如何阻止这种情况发生”。
  • 我认为缺少的一件事是提到的两个容器的析构函数(std::list 和std::deque)具有线性复杂性析构函数。因此,破坏可能需要很长时间也就不足为奇了。
  • 再想一想,你可能是对的。最初我会认为 OP 是在询问其他两个答案。
  • 谢谢,但是......首先,这个例子是一个无操作的例子,因为它是一个最小的例子,但是当我添加一些操作时(例如一个for循环,它对@ 987654325@) 它在 MSVC 发布模式下仍然运行不到一秒,在我尝试过的任何其他 MSVC 或 GCC 模式下都非常慢。其次,即使列表中的所有项目都包含在内,用 33 秒来销毁 240 MB 内存仍然没有任何意义!相信这实际上不是预期的行为,并且可能表明存在某种错误。
【解决方案4】:

在调试模式下真的很慢。

MSDN:

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

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

【讨论】:

  • 设置此环境变量后,程序在MSVC调试模式下运行仍然很慢。此外,这并不能解释为什么它在 GCC 上如此缓慢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-31
  • 1970-01-01
  • 2014-11-10
  • 1970-01-01
相关资源
最近更新 更多