Похоже, что общая концепция заключается в том, что 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 ;-)
}
Компиляция кода с помощью -O3 -o -march=native -std=c++14 -g
с gcc 7.1.0, clang 3.8.0 и 17.0.4 на Linux на Intel Xeon E5-2690 v3 @2.6 ГГц (без турбо):
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
, по-видимому, сначала скопирует векторные данные из одного блока памяти в другой и назначает исходный нулевой - все в ужасно не оптимизированном цикле. И затем он снова перебирает исходный блок данных, чтобы узнать, отличен ли какой-либо из тех, которые были только нулевыми, и их необходимо удалить. Полную информацию о gory можно увидеть на 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%. Удивительно, что проверка удаления последовательно опущена для:
auto tmp = b.release();
a.release();
a.reset(tmp);
Эти биты не являются основным аспектом этого вопроса, но все это заставляет меня чувствовать, что я чего-то не хватает.
Почему различные компиляторы не могут оптимизировать перераспределение в std::vector<std::unique_ptr<int>>
? Есть ли что-нибудь в стандарте, которое предотвращает генерацию кода так же эффективно, как с необработанными указателями? Это проблема стандартной реализации библиотеки? Или компиляторы недостаточно недостаточно умны (пока)?
Что можно сделать, чтобы избежать влияния производительности по сравнению с использованием raw-указателей?
Примечание. Предположим, что T
является полиморфным и дорогим для перемещения, поэтому std::vector<T>
не является вариантом.