【问题标题】:why std::vector::push_back much slower than a manual implementation?为什么 std::vector::push_back 比手动实现慢得多?
【发布时间】:2021-05-11 10:19:42
【问题描述】:

本来想写一个trivial类型的动态数组(然后我可以用memcpy或者sth来优化),但是当我将它的效率与std::vector进行比较时,我发现它的push_back函数的效率是a@的两倍987654323@.

这太奇怪了,我看了MSVC STL的源代码才知道原因,但没有成功。

我的代码:


template<typename T>
class Array {
    static_assert((std::is_trivial_v<T>), "Array requires type to be trivial.");
    T* m_p;
    size_t m_cap, m_siz;
private:
    void increase(size_t new_cap) {
        ASSERT(new_cap > m_cap);
        if (new_cap < m_cap + m_cap / 2)new_cap = m_cap + m_cap / 2;
        T* new_p = (T*)realloc(m_p, sizeof(T) * new_cap);
        ASSERT(new_p);
        m_p = new_p;
        m_cap = new_cap;
    }
public:
    Array() : m_siz(0), m_cap(0), m_p(nullptr) {}
    ~Array() { if (m_p)free(m_p); }
    Array(const Array& x) {
        m_cap = m_siz = x.m_siz;
        m_p = (T*)malloc(sizeof(T) * m_siz);
        memcpy(m_p, x.m_p, sizeof(T) * m_siz);
    }
    Array(Array&& x) {
        m_cap = x.m_cap;
        m_siz = x.m_siz;
        m_p = x.m_p;
        x.m_p = nullptr;
        x.m_cap = x.m_siz = 0;
    }
    Array& operator=(const Array& x) {
        m_cap = m_siz = x.m_siz;
        m_p = (T*)malloc(sizeof(T) * m_siz);
        memcpy(m_p, x.m_p, sizeof(T) * m_siz);
    }
    Array& operator=(Array&& x) {
        m_cap = x.m_cap;
        m_siz = x.m_siz;
        m_p = x.m_p;
        x.m_p = nullptr;
        x.m_cap = x.m_siz = 0;
    }
    void push_back(const T& x) {
        if (m_siz == m_cap)increase(m_cap + 1);
        m_p[m_siz++] = x;
    }
    void reserve(size_t cap) {
        if (cap > m_cap)
            increase(cap);
    }
    void resize(size_t siz, const T& val = T{}) {
        if (siz > m_siz) {
            if (siz > m_cap)increase(siz);
            for (size_t i = m_siz; i < siz; i++)
                m_p[i] = val;
        }
        m_siz = siz;
    }
    void shrink_to_fit() {
        m_p = realloc(m_p, m_siz * sizeof(T));
        m_cap = m_siz;
    }
    T& operator[](size_t pos) { ASSERT(pos < m_siz); return m_p[pos]; }
    const T& operator[](size_t pos) const { ASSERT(pos < m_siz); return m_p[pos]; }
    size_t size()const { return m_siz; }
    size_t capacity()const { return m_cap; }
    void clear() { m_siz = 0; }
};

#define N 10000
#define M 100000
template<typename T>
int test() {
    int t = clock();
    T v;
    v.reserve(M);
    for (int t = 0; t < N; t++) {
        for (int i = 0; i < M; i++)
            v.push_back(i);
        //v.resize(M);

        //for (int i = 0; i < M; i++)
            //v[i] = -i;
        v.clear();
    }
    return clock() - t;
}
int main() {
    int t1 = test<std::vector<int>>();
    int t2 = test<Array<int>>();

    printf("vector:%dms.\nArray:%dms.\n", t1, t2);
}

结果(在 VS2019 中发布):

vector:1043ms.
Array:547ms.

这是std::vector::pushback(MSVC)的调用链:

    void push_back(const _Ty& _Val) { // insert by moving into element at end, provide strong guarantee
        emplace_back(_STD move(_Val));
    }

    template <class... _Valty>
    decltype(auto) emplace_back(_Valty&&... _Val) {
        // insert by perfectly forwarding into element at end, provide strong guarantee
        auto& _My_data   = _Mypair._Myval2;
        pointer& _Mylast = _My_data._Mylast;
        if (_Mylast != _My_data._Myend) {
            return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
        }

        _Ty& _Result = *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
#if _HAS_CXX17
        return _Result;
#else // ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv
        (void) _Result;
#endif // _HAS_CXX17
    }


    template <class... _Valty>
    decltype(auto) _Emplace_back_with_unused_capacity(_Valty&&... _Val) {
        // insert by perfectly forwarding into element at end, provide strong guarantee
        auto& _My_data   = _Mypair._Myval2;
        pointer& _Mylast = _My_data._Mylast;
        _STL_INTERNAL_CHECK(_Mylast != _My_data._Myend); // check that we have unused capacity
        _Alty_traits::construct(_Getal(), _Unfancy(_Mylast), _STD forward<_Valty>(_Val)...);
        _Orphan_range(_Mylast, _Mylast);
        _Ty& _Result = *_Mylast;
        ++_Mylast;
#if _HAS_CXX17
        return _Result;
#else // ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv
        (void) _Result;
#endif // _HAS_CXX17
    }

经过内联和其他优化后,STL 代码似乎与我的没有太大区别。

谁能告诉我为什么我的 push_back 更有效(或者为什么 STL 很慢)?

编辑: 抱歉,我的问题似乎引起了误解。

我知道std::vector 是通用的,所以它可能比我做的工作更多。

但是在 int 类型的 push_back 中,我不认为 std::vector 做了什么特别的事情,为什么它很慢?

【问题讨论】:

  • 您实际上是在创建/复制对象吗? realloc / malloc 只是分配内存。
  • static_assert((std::is_trivial_v&lt;T&gt;), "Array requires type to be trivial."); 这不是std::vector 是什么。您始终可以删除功能并获得性能。棘手的部分是遵守std::vector 的规范并且比一些现有的实现更有效
  • 尝试使用你的std::string。我不确定在哪里,但我几乎可以肯定它会爆炸
  • 当然这对push_back 很重要。当请求的大小超过当前容量时,您需要重新分配和移动元素。而你只是分配内存,这通常是不够的
  • @largest_prime_is_463035818 You need to create them 不再是,因为可以隐式创建琐碎的对象,因为 P0593R6 已被接受为标准作为缺陷解决方案。 Malloc 和 assign 现在正式确定了。

标签: c++ vector stl


【解决方案1】:

您的版本仅对可简单复制的类型有效。

因为重新分配可能涉及按字节复制(无论是扩展还是收缩),只有 TriviallyCopyable 类型的对象可以在内存块的保留部分中安全访问致电realloc

普通std::vector的要求比较宽松,一般只有DestructibleCopyInsertableMoveInsertable之一push_back

【讨论】:

  • 但是我之前预留了足够的内存,当我使用 resize 而不是 reserve 时,我得到了相同的结果
  • @victor12369 您需要了解您只考虑了一个非常狭窄的用例。 std::vector 可以做更多,你的向量不能。牵强的类比:2+2 == 2*2,但你不会得出结论说我们现在总是可以使用+ 而不是*,因为无论如何它们会产生相同的结果,而+ 要简单得多
  • realloc使用std::string 的数组是未定义的行为
  • @victor12369 我们正在转圈...... std::vector&lt;std::string&gt; 是一回事。 Array&lt;std::string&gt; 立即失败
  • 不,实际上它不会被调用,因为我保留了足够的内存@Caleth
【解决方案2】:

由于内存中的布局不同,测量的差异完全有可能是偶然的。布局对性能的影响很大,除了矢量的实现之外,它还可能受到许多因素的影响。

我在另一个系统上运行了您的基准测试,您的向量完成时间比标准系统长 31%。诚然,向量实现是不同的——libstdc++。但这并不意味着 libstdc++ 向量比您的向量快,后者比 MSVC 向量快。基准根本不够复杂,无法自信地确定这一点。

【讨论】:

    【解决方案3】:

    差异主要是由于std::vector 版本中不太受欢迎的代码生成。如果我们比较生成的程序集(godbolt link),我们可以看到它。

    您的循环(跳过重新分配部分):

    $LL4@test:
            xor     esi, esi
            xor     edi, edi
    $LL7@test:
            mov     r15, rbx
            cmp     rdi, rbx
            jne     SHORT $LN27@test
            <...skip...>
    $LN27@test:
            mov     DWORD PTR [r14+rdi*4], esi
            inc     rdi
            inc     esi
            cmp     esi, 100000                         ; 000186a0H
            jl      $LL7@test
            sub     r12, r13
            jne     $LL4@test
    

    std::vector::push_back 循环(同样,跳过重新分配部分):

    $LL4@test:
            xor     ebx, ebx
            mov     DWORD PTR i$1[rsp], ebx
    $LL7@test:
            cmp     rcx, QWORD PTR v$[rsp+16]
            je      SHORT $LN26@test
            mov     DWORD PTR [rcx], ebx
            mov     rcx, QWORD PTR v$[rsp+8]
            add     rcx, 4
            mov     QWORD PTR v$[rsp+8], rcx
            jmp     SHORT $LN5@test
    $LN26@test:
            <...skip...>
    $LN5@test:
            inc     ebx
            mov     DWORD PTR i$1[rsp], ebx
            cmp     ebx, 100000                         ; 000186a0H
            jl      SHORT $LL7@test
            mov     rcx, QWORD PTR v$[rsp]
            mov     QWORD PTR v$[rsp+8], rcx
            sub     rdi, 1
            jne     SHORT $LL4@test
    

    很明显,我们可以看到更多的代码(11 对 8 条指令在热路径中)和更多的内存间接性(5 对 1)。所以速度变慢也就不足为奇了。

    更一般地说,更复杂的代码 == 更慢的代码。

    这两个版本可以同样优化吗?我看不出为什么不这样做。 MSVC 19.28 做不到。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-10
      • 2017-01-08
      • 2014-08-27
      • 2021-03-11
      • 2015-09-26
      • 2012-10-19
      • 1970-01-01
      相关资源
      最近更新 更多