一般认为,与正确使用的拥有原始指针相比,std::unique_ptr
在没有时间开销的情况下具有更好的性能, 只要进行足够的优化。
但是,在复合数据结构中使用std::unique_ptr
,特别是在std::vector<std::unique_ptr<T>>
中使用,会怎么样呢?例如,调整向量的基础数据大小,这可能会在push_back
期间发生。为了隔离性能,我循环执行pop_back
、shrink_to_fit
、emplace_back
:
#include <chrono>
#include <vector>
#include <memory>
#include <iostream>
constexpr size_t size = 1000000;
constexpr size_t repeat = 1000;
using my_clock = std::chrono::high_resolution_clock;
template<class T>
auto test(std::vector<T>& v) {
v.reserve(size);
for (size_t i = 0; i < size; i++) {
v.emplace_back(new int());
}
auto t0 = my_clock::now();
for (int i = 0; i < repeat; i++) {
auto back = std::move(v.back());
v.pop_back();
v.shrink_to_fit();
if (back == nullptr) throw "don't optimize me away";
v.emplace_back(std::move(back));
}
return my_clock::now() - t0;
}
int main() {
std::vector<std::unique_ptr<int>> v_u;
std::vector<int*> v_p;
auto millis_p = std::chrono::duration_cast<std::chrono::milliseconds>(test(v_p));
auto millis_u = std::chrono::duration_cast<std::chrono::milliseconds>(test(v_u));
std::cout << "raw pointer: " << millis_p.count() << " ms, unique_ptr: " << millis_u.count() << " ms\n";
for (auto p : v_p) delete p; // I don't like memory leaks ;-)
}
使用gcc 7.1.0、clang 3.8.0和17.0.4在Linux上编译带有
-O3 -o -march=native -std=c++14 -g
参数的代码,运行环境为Intel Xeon E5-2690 v3 @ 2.6 GHz(无turbo)。raw pointer: 2746 ms, unique_ptr: 5140 ms (gcc)
raw pointer: 2667 ms, unique_ptr: 5529 ms (clang)
raw pointer: 1448 ms, unique_ptr: 5374 ms (intel)
原始指针版本的代码主要花费时间在优化的 memmove
上(intel 的比clang和gcc好得多)。而 unique_ptr
的代码似乎首先将向量数据从一个内存块复制到另一个内存块,然后将原始内存块赋值为零 - 这全部在一个可怕的未优化循环中完成。然后,它再次循环遍历原始数据块,以查看那些刚刚被清零的数据是否为非零并需要被删除。完整的细节可以在 godbolt 上看到。 问题不在于编译后的代码有何差异,这是非常明显的。问题在于编译器无法优化通常被视为没有额外开销的抽象。
为了理解编译器处理 std::unique_ptr
的方式,我更仔细地查看了一些孤立的代码。例如:
void foo(std::unique_ptr<int>& a, std::unique_ptr<int>& b) {
a.release();
a = std::move(b);
}
或类似的
a.release();
a.reset(b.release());
目前没有任何一种 x86 编译器能够优化掉无意义的代码 if (ptr) delete ptr;
。甚至 Intel 编译器也有 28% 的概率保留 delete 操作。令人惊讶的是,删除检查在以下情况下始终被省略:
auto tmp = b.release();
a.release();
a.reset(tmp);
这些位不是这个问题的主要方面,但所有这一切都让我感觉到我错过了什么。
为什么各种编译器无法优化
std::vector<std::unique_ptr<int>>
中的重新分配?标准中是否有任何东西阻止生成与原始指针一样有效的代码?这是标准库实现的问题吗?还是编译器还不够聪明(尚未)?与使用原始指针相比,有什么方法可以避免性能影响?
注意:假设T是多态的且移动成本高,因此
std::vector<T>
不是选项。
shrink_to_fit()
不一定需要真正做任何事情。在unique_ptr
的情况下它可能会有所作为,但对于原始指针情况可能并非如此。 - NathanOlivernullptr
时,std::unique_ptr<>::pop_back
不会删除已被移动的指针。 - Zulanunique_ptr
的情况,而编译器则可以通过简单调用memcpy
来移动T*
。 - Justin