当不存在 xvalue 时,移动语义如何应用于以下代码段?
How does move semantics apply on the following snippet when no xvalue is present?
我偶然发现了 following article 并且不理解 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
是一个左值,并且此代码段在 C++98 和 C++11 中调用了相同的 vector::push_back(const T&)
。
代码在任一版本上的编译结果相同:https://godbolt.org/z/q3Lzae
是作者的说法不正确,还是编译器足够聪明,意识到x
即将被摧毁?
如果作者不正确,C++11 中是否还有其他任何东西可以提高性能 "without changing a line of code"?
你说得对,对象 x
不会被移走。获得性能的移动操作与 V
.
中已有的其他 k
向量有关
随着向量的增长(除非 reserve
使用了足够大的大小),它有时需要重新分配以获得更大的内存块,因为它的元素需要在连续的内存中。这不会发生在每个 push_back
上,但在本例中有时肯定会发生。所以假设 push_back
和其他函数使用一些私有函数 grow_capacity
,它获得足够的内存,然后在该内存中的向量中创建已经存在的对象。
在 C++03 中,对于任意模板参数 T
,在新内存中创建对象的唯一合理方法是使用 T
.[=32 的复制构造函数=]
// 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>>
容器可以只使用那个移动构造函数,这只是一些标量成员的交换,也导致旧对象的析构函数什么也不做。
这个例子中发生的事情是 x
即将超出 push_back 调用的范围(它的生命周期结束,并且没有后续使用),所以编译器 可能 将其视为一个 xvalue 并移出它。它不是编译器需要进行移动优化的情况之一,所以它可能不会,但如果启用了优化,任何体面的编译器都会这样做(gcc 和 clang 都会在这里使用移动)。
我偶然发现了 following article 并且不理解 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
是一个左值,并且此代码段在 C++98 和 C++11 中调用了相同的 vector::push_back(const T&)
。
代码在任一版本上的编译结果相同:https://godbolt.org/z/q3Lzae
是作者的说法不正确,还是编译器足够聪明,意识到x
即将被摧毁?
如果作者不正确,C++11 中是否还有其他任何东西可以提高性能 "without changing a line of code"?
你说得对,对象 x
不会被移走。获得性能的移动操作与 V
.
k
向量有关
随着向量的增长(除非 reserve
使用了足够大的大小),它有时需要重新分配以获得更大的内存块,因为它的元素需要在连续的内存中。这不会发生在每个 push_back
上,但在本例中有时肯定会发生。所以假设 push_back
和其他函数使用一些私有函数 grow_capacity
,它获得足够的内存,然后在该内存中的向量中创建已经存在的对象。
在 C++03 中,对于任意模板参数 T
,在新内存中创建对象的唯一合理方法是使用 T
.[=32 的复制构造函数=]
// 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>>
容器可以只使用那个移动构造函数,这只是一些标量成员的交换,也导致旧对象的析构函数什么也不做。
这个例子中发生的事情是 x
即将超出 push_back 调用的范围(它的生命周期结束,并且没有后续使用),所以编译器 可能 将其视为一个 xvalue 并移出它。它不是编译器需要进行移动优化的情况之一,所以它可能不会,但如果启用了优化,任何体面的编译器都会这样做(gcc 和 clang 都会在这里使用移动)。