【发布时间】:2017-12-18 17:35:58
【问题描述】:
一般的概念似乎是std::unique_ptr 具有no time overhead 与正确使用的拥有原始指针given sufficient optimization 相比。
但是在复合数据结构中使用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 ;-)
}
在 Intel Xeon E5-2690 v3 @ 2.6 GHz(无涡轮)的 Linux 上使用 gcc 7.1.0、clang 3.8.0 和 17.0.4 编译代码:
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 编译器 seem to be able to optimize away 无意义的 if (ptr) delete ptr;。英特尔编译器甚至给了删除 28% 的机会。令人惊讶的是,删除检查始终被省略:
auto tmp = b.release();
a.release();
a.reset(tmp);
这些点不是这个问题的主要方面,但所有这些都让我觉得我错过了一些东西。
为什么各种编译器无法优化std::vector<std::unique_ptr<int>> 内的重新分配?标准中是否有任何内容阻止生成与原始指针一样高效的代码?这是标准库实现的问题吗?还是编译器不够聪明(还)?
与使用原始指针相比,可以做些什么来避免性能影响?
注意:假设T 是多态的并且移动成本很高,所以std::vector<T> 不是一个选项。
【问题讨论】:
-
默认构造的
std::unique_ptr可能会导致几乎NOP。 -
我很确定如果你将
back声明为volatile,编译器不会优化它,你也不需要那个异常技巧。 -
推理巨大的缓冲区和它们的 unform 状态很难吗?以某种方式教编译器
std::unique_ptr可以用源零破坏性地不进行位块移动,并且向量可以使用破坏性的不位块移动,而不是教它指向的 100000 个元素的状态更容易前面的代码行可以证明以下几点。不久前,我确实在一个标准提案中看到了 nothrow move-and-destroy 特性,我不知道它的状态。 -
godbolt 对于探索这类问题很有用(如果您是汇编语言)。
-
unique_ptr的情况可能必须通过调用移动构造函数来移动所有元素,而编译器能够通过简单地调用memcpy来移动T*s
标签: c++ c++11 vector compiler-optimization unique-ptr