【问题标题】:vector<T>::push_back is used on predefined constructor?vector<T>::push_back 用于预定义的构造函数?
【发布时间】:2018-04-19 01:28:17
【问题描述】:

我来这里是想问问我的看法是否真实。 我原本以为定义vector&lt;T&gt; v(size_t someSize, T init_value) 会调用vector&lt;T&gt;::reserve 之类的函数,而不是vector&lt;T&gt;::push_back。我在这里找到了一些与此相关的讨论:std::vector push_back is bottleneck,但这与它的想法略有不同。

运行一些实验,我注意到vector&lt;T&gt; v(size_t someSize, T init_value) 一直调用::push_back。这是真的?我有以下使用uftrace(https://github.com/namhyung/uftrace) 的报告。

     Avg total   Min total   Max total  Function
==========  ==========  ==========  ====================================
858.323 ms  858.323 ms  858.323 ms  main
618.245 ms  618.245 ms  618.245 ms  sortKaway
234.795 ms  234.795 ms  234.795 ms  std::sort
 72.752 us   72.752 us   72.752 us  std::vector::_M_fill_initialize
 65.788 us   49.551 us   82.026 us  std::vector::vector
 20.292 us   11.387 us   68.629 us  std::vector::_M_emplace_back_aux
 18.722 us   17.263 us   20.181 us  std::equal
 18.472 us   18.472 us   18.472 us  std::vector::~vector
 17.891 us   10.002 us  102.079 us  std::vector::push_back // push_back?!

vector&lt;T&gt;::reserve 最终也会调用vector&lt;t&gt;::push_back 吗? vector 有更快的版本吗?


以上为原帖。经过一些cmet之后,我测试了一个简单的版本,然后意识到我完全错了。

#include <vector>
#include <functional>
#include <queue>
#include <cassert>
using namespace std; // for the time being

int main () {
    vector<int> v(10, 0);

    return 0;
}

这实际上导致如下,不涉及std::vector&lt;T&gt;::push_back

  # Function Call Graph for 'main' (session: 9ce7f6bb33885ff7)
  =============== BACKTRACE ===============
   backtrace #0: hit 1, time  12.710 us
     [0] main (0x4009c6)

  ========== FUNCTION CALL GRAPH ==========
    12.710 us : (1) main
     0.591 us :  +-(1) std::allocator::allocator
     0.096 us :  | (1) __gnu_cxx::new_allocator::new_allocator
              :  | 
     6.880 us :  +-(1) std::vector::vector
     4.338 us :  |  +-(1) std::_Vector_base::_Vector_base
     0.680 us :  |  |  +-(1) std::_Vector_base::_Vector_impl::_Vector_impl
     0.445 us :  |  |  | (1) std::allocator::allocator
     0.095 us :  |  |  | (1) __gnu_cxx::new_allocator::new_allocator
              :  |  |  | 
     3.294 us :  |  |  +-(1) std::_Vector_base::_M_create_storage
     3.073 us :  |  |    (1) std::_Vector_base::_M_allocate
     2.849 us :  |  |    (1) std::allocator_traits::allocate
     2.623 us :  |  |    (1) __gnu_cxx::new_allocator::allocate
     0.095 us :  |  |     +-(1) __gnu_cxx::new_allocator::max_size
              :  |  |     | 
     1.867 us :  |  |     +-(1) operator new
              :  |  | 
     2.183 us :  |  +-(1) std::vector::_M_fill_initialize
     0.095 us :  |     +-(1) std::_Vector_base::_M_get_Tp_allocator
              :  |     | 
     1.660 us :  |     +-(1) std::__uninitialized_fill_n_a
     1.441 us :  |       (1) std::uninitialized_fill_n
     1.215 us :  |       (1) std::__uninitialized_fill_n::__uninit_fill_n
     0.988 us :  |       (1) std::fill_n
     0.445 us :  |        +-(1) std::__niter_base
     0.096 us :  |        | (1) std::_Iter_base::_S_base
              :  |        | 
     0.133 us :  |        +-(1) std::__fill_n_a

很抱歉给您带来了困惑。是的,库实现如我们预期的那样工作,如果使用initial size 构建,它不涉及push_back

【问题讨论】:

  • 省略号 ("...") 在 c++ 中有特定的含义。在你的问题中特别是在代码块中使用它时要小心,因为它可能会导致混淆。
  • reserve 的一个常见用例是预留一次,然后再预留几次push_back。这两个功能并不相互排斥,实际上经常一起使用。
  • @FrançoisAndrieux 你说得对,我应该多加注意
  • 虽然标准对构造函数重载的工作方式没有太多限制,但我真的怀疑任何好的实现都会在不先保留大小的情况下做到这一点......你使用的是什么编译器/标准库?你的构建标志是什么?
  • 你能提供你的实验代码吗?它是否在做任何其他可能导致std::vector::push_back 被调用的事情?我可以在 gcc 标准库源代码中看到您所指的构造函数调用std::vector::_M_fill_initialize,但我不确定它为什么会调用push_back

标签: c++ performance vector


【解决方案1】:

唷,看起来你回答了自己的问题!一时间,我非常的困惑。只是假设,我可以想象一些使用reservepush_backsvector's 填充构造函数的模糊案例,但绝对不是像在GNU 标准库中发现的GCC 那样的高质量实现。我想说,假设性地,一个晦涩的向量实现可以通过这种方式实现,但实际上完全不可能实现任何体面的实现。

相反,这几乎是 20 年前的事了,但我尝试实现我的 std::vector 版本,希望能匹配它的性能。这不仅仅是一些愚蠢的练习,而是因为我们有一个软件开发工具包并希望为它使用一些基本的 C++ 容器,但它的目标是允许人们使用不同的方式为我们的软件编写插件。编译器(以及不同的标准库实现)而不是我们使用的。所以我们不能在这些情况下安全地使用std::vector,因为我们的版本可能与插件作者的不匹配。我们不得不勉强为 SDK 推出自己的容器。

相反,我发现std::vector 在难以匹敌的方式上非常高效,尤其是对于具有微不足道的 ctor 和 dtor 的普通旧数据类型。这又是十多年前的事了,但我发现在 MSVC 5 或 6 中使用带有 vector&lt;int&gt; 的填充构造函数(忘了哪个)实际上转换为与使用 memset 相同的反汇编,以我的幼稚版本的方式,只是循环无论它们是否是 POD,都没有在它们上使用新的位置。 range ctor 还有效地转换为 POD 的超快memcpy。这正是让我难以击败vector的原因,至少在当时是这样。如果不深入研究类型特征和特殊外壳 POD,我无法真正匹配矢量在 POD 上的性能。我可以将它与 UDT 相匹配,但我们的大多数性能关键代码都倾向于使用 POD。

因此,今天流行的矢量实现很可能与我进行这些测试时一样有效,而且我想在某种程度上保证您的矢量实现很可能很快。我希望它做的最后一件事是使用 push_backs 实现填充 ctor 或范围 ctor。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-05-06
    • 1970-01-01
    • 2012-06-01
    • 1970-01-01
    • 2020-06-05
    • 1970-01-01
    • 2012-07-14
    • 1970-01-01
    相关资源
    最近更新 更多