我想为平凡类型写一个动态数组(然后我可以使用memcpy或sth来优化),但是当我将其效率与std::vector
进行比较时,我发现它的push_back
函数是其两倍高效std::vector
。
这太奇怪了,我阅读了 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
是通用的,所以它可能比我做的更多。
但是在 push_back
类型的 int
中,我不认为 std::vector
做了什么特别的事情,为什么它很慢?
答案 0 :(得分:2)
您的版本仅对可简单复制的类型有效。
<块引用>因为重新分配可能涉及逐字节复制(无论是扩展还是收缩),只有 TriviallyCopyable 类型的对象在内存块的保留部分可以安全访问在调用 realloc
之后。
普通的 std::vector
在比较中的要求非常宽松,一般只是 Destructible 和 CopyInsertable 之一或MoveInsertable push_back
。
答案 1 :(得分:2)
测量的差异完全有可能是偶然的,因为内存中的布局不同。布局对性能的影响很大,除了向量的实现之外,它还会受到很多因素的影响。
我在另一个系统上运行了您的基准测试,并且您的向量完成时间比标准向量长 31%。不可否认,向量实现是不同的 - libstdc++。但这并不意味着 libstdc++ 向量比你的向量快,后者比 MSVC 向量快。基准测试不够复杂,无法自信地确定这一点。
答案 2 :(得分:1)
差异主要是由于 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 就是做不到。