La conception générale semble être que l' std::unique_ptr
a pas de surcharge de temps par rapport à correctement utilisé de posséder raw pointeurs, suffisamment à l'optimisation.
Mais que penser de l'utilisation std::unique_ptr
dans le composé de structures de données, en particulier std::vector<std::unique_ptr<T>>
? Par exemple, le redimensionnement de données sous-jacentes d'un vecteur, qui peut se produire lors push_back
. Pour isoler les performances, je boucle autour de 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 ;-)
}
Compiler le code avec -O3 -o -march=native -std=c++14 -g
avec gcc 7.1.0, clang 3.8.0, et 17.0.4 sur Linux sur un processeur Intel Xeon E5-2690 v3 @ 2.6 GHz (pas de 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)
Le pointeur brut version passe tous il est temps de manière optimale memmove
(intel semble avoir une bien meilleur que clang et gcc). L' unique_ptr
code semble d'abord copier le vecteur des données à partir d'un bloc de mémoire à l'autre et d'attribuer l'origine à zéro - le tout dans une horrible et non optimisée de la boucle. Et puis, il passe en boucle sur l'origine du bloc de données à nouveau pour voir si l'un de ceux qui ont été seulement zéro avais sont non nuls et doivent être supprimés. Les sanglants pleins de détails peuvent être vus sur godbolt. La question n'est pas comment le code compilé diffère, c'est assez clair. La question est de savoir pourquoi le compilateur ne parvient pas à optimiser ce qui est généralement considéré comme une extra-frais généraux de l'abstraction.
Essayant de comprendre comment les compilateurs raison au sujet de la manipulation std::unique_ptr
, j'étais à la recherche d'un peu plus isolées code. Par exemple:
void foo(std::unique_ptr<int>& a, std::unique_ptr<int>& b) {
a.release();
a = std::move(b);
}
ou similaire
a.release();
a.reset(b.release());
aucun des x86 compilateurs semblent être en mesure d'optimiser loin l'absurde if (ptr) delete ptr;
. Le compilateur Intel donne même le supprimer de 28 % de chance. Étonnamment, la suppression de la vérification est systématiquement omis pour:
auto tmp = b.release();
a.release();
a.reset(tmp);
Ces bits ne sont pas le principal aspect de cette question, mais tout cela me fait sentir que je suis absent quelque chose.
Pourquoi ne divers compilateurs ne parviennent pas à optimiser la réaffectation au sein d' std::vector<std::unique_ptr<int>>
? Il n'y a rien dans la norme qui empêche la génération de code aussi efficace qu'avec des pointeurs? Est-ce un problème avec la bibliothèque standard de mise en œuvre? Ou sont les compilateurs tout simplement pas suffisamment assez intelligent (encore)?
Que peut-on faire pour éviter l'impact sur les performances par rapport à l'aide de matières pointeurs?
Remarque: Supposons que, T
est polymorphe et coûteux pour se déplacer, alors std::vector<T>
n'est pas une option.