我无意中发现了下列条款,并且不理解C++98和C++11之间的性能差异,正如作者所说,这归因于移动语义。
#include <vector>
using namespace std;
int main() {
vector<vector<int> > V;
for(int k = 0; k < 100000; ++k) {
vector<int> x(1000);
V.push_back(x);
}
return 0;
}据我所知,V.push_back(x)不调用任何移动语义。我相信x是一个lvalue,这个片段在C++98和C++11中都调用相同的vector::push_back(const T&)。
代码在任何一个版本上都编译相同:https://godbolt.org/z/q3Lzae
作者的陈述是否不正确,或者编译器是否聪明到足以意识到x即将被销毁?
如果作者不正确,那么C++11中还有什么东西可以“不改变一行代码”地提高性能呢?
发布于 2020-02-04 02:07:14
你是对的,x是不会被移动的。获得性能的移动操作与已经在k中的其他V向量有关。
随着向量的增长(除非reserve具有足够的大小),它有时需要重新分配以获得更大的内存块,因为它的元素必须位于连续内存中。并不是每个push_back都会发生这种情况,但在本例中肯定会发生这种情况。假设push_back和其他函数使用了一些私有函数grow_capacity,它获得了足够的内存,然后在该内存中的向量中创建对象。
在C++03中,为任意模板参数T在新内存中创建对象的唯一合理方法是使用T的复制构造函数。
// C++03 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
T* new_data = get_allocator().allocate(new_capacity);
T* new_end = new_data;
try {
for (const_iterator iter = begin(); iter != end(); ++iter) {
::new(static_cast<void*>(new_end)) T(*iter); // T copy ctor!
++new_end;
}
} catch (...) {
while (new_end != new_data) (--new_end)->~T();
get_allocator().deallocate(new_data, new_capacity);
throw;
}
// Clean up old objects and memory.
for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
riter->~T();
get_allocator().deallocate(_data, _capacity);
// Assign private members.
_data = new_data;
_capacity = new_capacity;
}在C++11和以后的版本中,当std::vector<T>需要重新分配到更大的容量时,允许移动它的T元素,而不是复制它们,如果它可以这样做的话,而不破坏强异常保证。这要求声明移动构造函数以避免引发任何异常。但是,如果移动构造函数可能抛出,则需要以旧的方式复制元素,以确保如果发生这种情况,向量将保持一致状态。
// C++17 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
T* new_data = get_allocator().allocate(new_capacity);
if constexpr (::std::is_nothrow_move_constructible_v<T>) {
::std::uninitialized_move(begin(), end(), new_data); // T move ctor!
} else {
T* new_end = new_data;
try {
for (const T& old_obj : *this) {
::new(static_cast<void*>(new_end)) T(old_obj); // T copy ctor!
++new_end;
}
} catch (...) {
while (new_end != new_data) (--new_end)->~T();
get_allocator().deallocate(new_data, new_capacity);
throw;
}
}
for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
riter->~T();
get_allocator().deallocate(_data, _capacity);
// Assign private members.
_data = new_data;
_capacity = new_capacity;
}因此,在具有std::vector<std::vector<int> >类型的容器中,T是std::vector<int>。以C++03方式增加容量有时需要大量的复制构造函数,然后是std::vector<int>的析构函数。每个复制构造函数分配一些内存并复制1000个int值,每个析构函数释放一些内存,因此这将真正地加起来。但是,对于C++11 std::vector,由于元素类型std::vector<int>确实有noexcept移动构造函数,所以std::vector<std::vector<int>>容器可以只使用该移动构造函数,这只是标量成员的几个交换,并且还会导致迁移的析构函数--从旧对象到旧对象--什么都不做。
发布于 2020-02-04 07:15:50
在这个例子中,x将在push_back调用中超出作用域(它的生命周期结束,并且没有后续使用),所以编译器可能会将它视为xvalue并移出它。这不是编译器需要进行移动优化的情况之一,所以它可能不会,但是如果启用了优化,那么任何体面的编译器都会这样做( gcc和clang都会在这里使用move )。
https://stackoverflow.com/questions/60049233
复制相似问题