Je m'interroge sur les performances de std::variant
. Quand ne dois-je pas l'utiliser? Il semble que les fonctions virtuelles soient encore bien meilleures que l'utilisation de std::visit
Ce qui m'a surpris!
Dans "A Tour of C++" Bjarne Stroustrup dit ceci à propos de pattern checking
Après avoir expliqué std::holds_alternatives
Et les méthodes overloaded
:
Ceci est fondamentalement équivalent à un appel de fonction virtuelle, mais potentiellement plus rapide. Comme pour toutes les allégations de performances, ce "potentiellement plus rapide" doit être vérifié par des mesures lorsque les performances sont critiques. Pour la plupart des utilisations, la différence de performances est insignifiante.
J'ai testé certaines méthodes qui me sont venues à l'esprit et voici les résultats:
http://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg
Vous obtiendrez un résultat différent si vous activez l'optimisation:
http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc
Voici le code que j'ai utilisé pour les benchmarks; Je suis sûr qu'il existe un meilleur moyen de mettre en œuvre et d'utiliser des variantes pour les utiliser au lieu de mots clés virtuels ( héritage vs std :: variant ):
a supprimé l'ancien code; regardez les mises à jour
Quelqu'un peut-il expliquer quelle est la meilleure façon de mettre en œuvre ce cas d'utilisation pour std::variant
Qui m'a permis de tester et de comparer:
J'implémente actuellement RFC 3986 qui est 'URI' et pour mon cas d'utilisation, cette classe sera davantage utilisée en tant que const et ne sera probablement pas beaucoup modifiée et il est plus probable pour l'utilisateur de utilisez cette classe pour trouver chaque partie spécifique de l'URI plutôt que de créer un URI; il était donc logique d'utiliser std::string_view
et de ne pas séparer chaque segment de l'URI dans son propre std::string
. Le problème était que j'avais besoin d'implémenter deux classes pour cela; un pour quand je n'ai besoin que d'une version const; et un autre lorsque l'utilisateur souhaite créer l'URI plutôt que d'en fournir un et de le rechercher.
J'ai donc utilisé un template
pour corriger ce qui avait ses propres problèmes; mais j'ai réalisé que je pouvais utiliser std::variant<std::string, std::string_view>
(ou peut-être std::variant<CustomStructHoldingAllThePieces, std::string_view>
); J'ai donc commencé des recherches pour voir si cela aide réellement à utiliser des variantes ou non. D'après ces résultats, il semble que l'utilisation de l'héritage et virtual
est mon meilleur pari si je ne veux pas implémenter deux classes const_uri
Et uri
différentes.
Que pensez-vous que je devrais faire?
Merci pour @gan_ d'avoir mentionné et résolu le problème de levage dans mon code de référence. http://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY
J'ai été surpris du résultat de l'enfer de try-catch mais grâce à ce commentaire qui a du sens maintenant.
J'ai supprimé la méthode try-catch
Car elle était vraiment mauvaise; et aussi changé au hasard la valeur sélectionnée et par l'apparence de celui-ci, je vois un repère plus réaliste. Il semble que virtual
ne soit pas la bonne réponse après tout. http://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt
http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (sans la fuite de mémoire lol)
J'ai supprimé les frais généraux de génération de nombres aléatoires (je l'ai déjà fait dans la dernière mise à jour, mais il semble que j'avais saisi la mauvaise URL pour le test de performance) et j'ai ajouté un EmptyRandom pour comprendre la base de génération de nombres aléatoires. Et aussi fait quelques petits changements dans Virtual mais je ne pense pas que cela ait affecté quoi que ce soit. http://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI
http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw (supprimé le virtuel afin que vous puissiez mieux comparer le reste d'entre eux)
comme Jorge Bellon dit dans les commentaires, je ne pensais pas au coût d'allocation; j'ai donc converti chaque référence pour utiliser des pointeurs. Cette indirection a un impact sur les performances bien sûr, mais c'est plus juste maintenant. Il n'y a donc actuellement aucune allocation dans les boucles.
Voici le code:
a supprimé l'ancien code; regardez les mises à jour
J'ai exécuté quelques repères jusqu'à présent. Il semble que g ++ fasse un meilleur travail d'optimisation du code:
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
EmptyRandom 0.756 ns 0.748 ns 746067433
TradeSpaceForPerformance 2.87 ns 2.86 ns 243756914
Virtual 12.5 ns 12.4 ns 60757698
Index 7.85 ns 7.81 ns 99243512
GetIf 8.20 ns 8.18 ns 92393200
HoldsAlternative 7.08 ns 7.07 ns 96959764
ConstexprVisitor 11.3 ns 11.2 ns 60152725
StructVisitor 10.7 ns 10.6 ns 60254088
Overload 10.3 ns 10.3 ns 58591608
Et pour clang:
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
EmptyRandom 1.99 ns 1.99 ns 310094223
TradeSpaceForPerformance 8.82 ns 8.79 ns 87695977
Virtual 12.9 ns 12.8 ns 51913962
Index 13.9 ns 13.8 ns 52987698
GetIf 15.1 ns 15.0 ns 48578587
HoldsAlternative 13.1 ns 13.1 ns 51711783
ConstexprVisitor 13.8 ns 13.8 ns 49120024
StructVisitor 14.5 ns 14.5 ns 52679532
Overload 17.1 ns 17.1 ns 42553366
En ce moment, pour clang, il est préférable d'utiliser l'héritage virtuel mais pour g ++, il vaut mieux utiliser holds_alternative
Ou get_if
Mais dans l'ensemble, std::visit
Ne semble pas être un bon choix pour presque tous mes repères jusqu'à présent.
Je pense que ce serait une bonne idée si la correspondance de motifs (des instructions de commutateur capables de vérifier plus de choses que des littéraux entiers) serait ajoutée au c ++, nous écririons du code plus propre et plus maintenable.
Je m'interroge sur les résultats de package.index()
. Cela ne devrait-il pas être plus rapide? Qu'est ce que ça fait?
Version de Clang: http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI
La version qui utilise One one
Au lieu de auto one = new One
Basée sur commentaire de Maxim Egorushkin : http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q = (ne change pas beaucoup le résultat)
J'ai fait quelques changements et les résultats sont très différents d'un compilateur à l'autre maintenant. Mais il semble que std::get_if
Et std::holds_alternatives
Sont les meilleures solutions. virtual
semble fonctionner mieux pour des raisons inconnues avec clang maintenant. Cela m'étonne vraiment parce que je me souviens que virtual
était meilleur en gcc. Et aussi std::visit
Est totalement hors compétition; dans cette dernière référence, c'est encore pire que la recherche vtable.
Voici le benchmark (exécutez-le avec GCC/Clang et aussi avec libstdc ++ et libc ++):
http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y
#include <benchmark/benchmark.h>
#include <array>
#include <variant>
#include <random>
#include <functional>
#include <algorithm>
using namespace std;
struct One {
auto get () const { return 1; }
};
struct Two {
auto get() const { return 2; }
};
struct Three {
auto get() const { return 3; }
};
struct Four {
auto get() const { return 4; }
};
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<std::mt19937::result_type> random_pick(0,3); // distribution in range [1, 6]
template <std::size_t N>
std::array<int, N> get_random_array() {
std::array<int, N> item;
for (int i = 0 ; i < N; i++)
item[i] = random_pick(rng);
return item;
}
template <typename T, std::size_t N>
std::array<T, N> get_random_objects(std::function<T(decltype(random_pick(rng)))> func) {
std::array<T, N> a;
std::generate(a.begin(), a.end(), [&] {
return func(random_pick(rng));
});
return a;
}
static void TradeSpaceForPerformance(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
int index = 0;
auto ran_arr = get_random_array<50>();
int r = 0;
auto pick_randomly = [&] () {
index = ran_arr[r++ % ran_arr.size()];
};
pick_randomly();
for (auto _ : state) {
int res;
switch (index) {
case 0:
res = one.get();
break;
case 1:
res = two.get();
break;
case 2:
res = three.get();
break;
case 3:
res = four.get();
break;
}
benchmark::DoNotOptimize(index);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
// Register the function as a benchmark
BENCHMARK(TradeSpaceForPerformance);
static void Virtual(benchmark::State& state) {
struct Base {
virtual int get() const noexcept = 0;
virtual ~Base() {}
};
struct A final: public Base {
int get() const noexcept override { return 1; }
};
struct B final : public Base {
int get() const noexcept override { return 2; }
};
struct C final : public Base {
int get() const noexcept override { return 3; }
};
struct D final : public Base {
int get() const noexcept override { return 4; }
};
Base* package = nullptr;
int r = 0;
auto packages = get_random_objects<Base*, 50>([&] (auto r) -> Base* {
switch(r) {
case 0: return new A;
case 1: return new B;
case 3: return new C;
case 4: return new D;
default: return new C;
}
});
auto pick_randomly = [&] () {
package = packages[r++ % packages.size()];
};
pick_randomly();
for (auto _ : state) {
int res = package->get();
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
for (auto &i : packages)
delete i;
}
BENCHMARK(Virtual);
static void FunctionPointerList(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::function<int()>;
std::size_t index;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return std::bind(&One::get, one);
case 1: return std::bind(&Two::get, two);
case 2: return std::bind(&Three::get, three);
case 3: return std::bind(&Four::get, four);
default: return std::bind(&Three::get, three);
}
});
int r = 0;
auto pick_randomly = [&] () {
index = r++ % packages.size();
};
pick_randomly();
for (auto _ : state) {
int res = packages[index]();
benchmark::DoNotOptimize(index);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(FunctionPointerList);
static void Index(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
for (auto _ : state) {
int res;
switch (package->index()) {
case 0:
res = std::get<One>(*package).get();
break;
case 1:
res = std::get<Two>(*package).get();
break;
case 2:
res = std::get<Three>(*package).get();
break;
case 3:
res = std::get<Four>(*package).get();
break;
}
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(Index);
static void GetIf(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
for (auto _ : state) {
int res;
if (auto item = std::get_if<One>(package)) {
res = item->get();
} else if (auto item = std::get_if<Two>(package)) {
res = item->get();
} else if (auto item = std::get_if<Three>(package)) {
res = item->get();
} else if (auto item = std::get_if<Four>(package)) {
res = item->get();
}
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(GetIf);
static void HoldsAlternative(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
for (auto _ : state) {
int res;
if (std::holds_alternative<One>(*package)) {
res = std::get<One>(*package).get();
} else if (std::holds_alternative<Two>(*package)) {
res = std::get<Two>(*package).get();
} else if (std::holds_alternative<Three>(*package)) {
res = std::get<Three>(*package).get();
} else if (std::holds_alternative<Four>(*package)) {
res = std::get<Four>(*package).get();
}
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(HoldsAlternative);
static void ConstexprVisitor(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
auto func = [] (auto const& ref) {
using type = std::decay_t<decltype(ref)>;
if constexpr (std::is_same<type, One>::value) {
return ref.get();
} else if constexpr (std::is_same<type, Two>::value) {
return ref.get();
} else if constexpr (std::is_same<type, Three>::value) {
return ref.get();
} else if constexpr (std::is_same<type, Four>::value) {
return ref.get();
} else {
return 0;
}
};
for (auto _ : state) {
auto res = std::visit(func, *package);
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(ConstexprVisitor);
static void StructVisitor(benchmark::State& state) {
struct VisitPackage
{
auto operator()(One const& r) { return r.get(); }
auto operator()(Two const& r) { return r.get(); }
auto operator()(Three const& r) { return r.get(); }
auto operator()(Four const& r) { return r.get(); }
};
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
auto vs = VisitPackage();
for (auto _ : state) {
auto res = std::visit(vs, *package);
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(StructVisitor);
static void Overload(benchmark::State& state) {
One one;
Two two;
Three three;
Four four;
using type = std::variant<One, Two, Three, Four>;
type* package = nullptr;
auto packages = get_random_objects<type, 50>([&] (auto r) -> type {
switch(r) {
case 0: return one;
case 1: return two;
case 2: return three;
case 3: return four;
default: return three;
}
});
int r = 0;
auto pick_randomly = [&] () {
package = &packages[r++ % packages.size()];
};
pick_randomly();
auto ov = overload {
[] (One const& r) { return r.get(); },
[] (Two const& r) { return r.get(); },
[] (Three const& r) { return r.get(); },
[] (Four const& r) { return r.get(); }
};
for (auto _ : state) {
auto res = std::visit(ov, *package);
benchmark::DoNotOptimize(package);
benchmark::DoNotOptimize(res);
pick_randomly();
}
}
BENCHMARK(Overload);
// BENCHMARK_MAIN();
Résultats pour le compilateur GCC:
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
TradeSpaceForPerformance 3.71 ns 3.61 ns 170515835
Virtual 12.20 ns 12.10 ns 55911685
FunctionPointerList 13.00 ns 12.90 ns 50763964
Index 7.40 ns 7.38 ns 136228156
GetIf 4.04 ns 4.02 ns 205214632
HoldsAlternative 3.74 ns 3.73 ns 200278724
ConstexprVisitor 12.50 ns 12.40 ns 56373704
StructVisitor 12.00 ns 12.00 ns 60866510
Overload 13.20 ns 13.20 ns 56128558
Résultats pour le compilateur clang (ce qui m'étonne):
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
TradeSpaceForPerformance 8.07 ns 7.99 ns 77530258
Virtual 7.80 ns 7.77 ns 77301370
FunctionPointerList 12.1 ns 12.1 ns 56363372
Index 11.1 ns 11.1 ns 69582297
GetIf 10.4 ns 10.4 ns 80923874
HoldsAlternative 9.98 ns 9.96 ns 71313572
ConstexprVisitor 11.4 ns 11.3 ns 63267967
StructVisitor 10.8 ns 10.7 ns 65477522
Overload 11.4 ns 11.4 ns 64880956
Meilleur benchmark jusqu'à présent (sera mis à jour): http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y (consultez également le GCC)
std::visit
semble encore manquer d'optimisations sur certaines implémentations. Cela étant dit, il y a un point central qui n'est pas très bien vu dans cette configuration de type laboratoire - qui est que variante la conception basée sur la pile est basée sur la pile par rapport au modèle --- héritage virtuel qui gravitent naturellement vers le tas. Dans un scénario réel, cela signifie que la disposition de la mémoire pourrait très bien être fragmentée (peut-être au fil du temps - une fois que les objets quittent le cache, etc.) - à moins qu'elle ne puisse être évitée d'une manière ou d'une autre. Le contraire est la conception basée sur variante qui peut être mise en page dans la mémoire des contigoues. Je crois que c'est un point extrêmement important à considérer lorsque les performances sont concernées et ne peuvent pas être sous-estimées.
Pour illustrer cela, considérez ce qui suit:
std::vector<Base*> runtime_poly_;//risk of fragmentation
vs.
std::vector<my_var_type> cp_time_poly_;//no fragmentation (but padding 'risk')
Cette fragmentation est quelque peu difficile à intégrer dans un test de référence comme celui-ci. Si c'est (aussi) dans le contexte de la déclaration de bjarne, je ne sais pas quand il a dit que cela pourrait potentiellement être plus rapide (ce qui, je crois, est vrai).
Une autre chose très importante à retenir pour le std::variant
la conception basée sur le fait que la taille de chaque élément utilise la taille du plus grand élément possible. Par conséquent, si les objets n'ont pas à peu près la même taille, cela doit être soigneusement pris en compte, car cela peut avoir un impact négatif sur le cache.
Compte tenu de ces points ensemble, il est difficile de dire lequel est le mieux à utiliser dans le cas général - mais il devrait être suffisamment clair si l'ensemble est un `` petit '' fermé de la même taille - le style de la variante montre un grand potentiel pour être plus rapide (comme le note bjarne).
Nous ne considérons désormais que les performances et il y a en effet d'autres raisons de choisir l'un ou l'autre modèle: Au final, il vous suffit de sortir du confort du `` laboratoire '' et de concevoir et comparer vos cas d'utilisation réels.
Vous pouvez tous les faire correspondre avec une implémentation de visite si vous pouvez garantir que la variante ne sera jamais vide par exception. Voici un seul visiteur qui correspond au virtuel ci-dessus et s'harmonise très bien avec les tables jmp. https://gcc.godbolt.org/z/kkjACx
struct overload : Fs... {
using Fs::operator()...;
};
template <typename... Fs>
overload(Fs...) -> overload<Fs...>;
template <size_t N, typename R, typename Variant, typename Visitor>
[[nodiscard]] constexpr R visit_nt(Variant &&var, Visitor &&vis) {
if constexpr (N == 0) {
if (N == var.index()) {
// If this check isnt there the compiler will generate
// exception code, this stops that
return std::forward<Visitor>(vis)(
std::get<N>(std::forward<Variant>(var)));
}
} else {
if (var.index() == N) {
return std::forward<Visitor>(vis)(
std::get<N>(std::forward<Variant>(var)));
}
return visit_nt<N - 1, R>(std::forward<Variant>(var),
std::forward<Visitor>(vis));
}
while (true) {
} // unreachable but compilers complain
}
template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(
std::variant<Args...> const &var, Visitor &&vis, Visitors &&... visitors) {
auto ol =
overload{std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...};
using result_t = decltype(std::invoke(std::move(ol), std::get<0>(var)));
static_assert(sizeof...(Args) > 0);
return visit_nt<sizeof...(Args) - 1, result_t>(var, std::move(ol));
}
template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(std::variant<Args...> &var,
Visitor &&vis,
Visitors &&... visitors) {
auto ol =
overload(std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...);
using result_t = decltype(std::invoke(std::move(ol), std::get<0>(var)));
static_assert(sizeof...(Args) > 0);
return visit_nt<sizeof...(Args) - 1, result_t>(var, std::move(ol));
}
template <class... Args, typename Visitor, typename... Visitors>
[[nodiscard]] constexpr decltype(auto) visit_nt(std::variant<Args...> &&var,
Visitor &&vis,
Visitors &&... visitors) {
auto ol =
overload{std::forward<Visitor>(vis), std::forward<Visitors>(visitors)...};
using result_t =
decltype(std::invoke(std::move(ol), std::move(std::get<0>(var))));
static_assert(sizeof...(Args) > 0);
return visit_nt<sizeof...(Args) - 1, result_t>(std::move(var), std::move(ol));
}
template <typename Value, typename... Visitors>
inline constexpr bool is_visitable_v = (std::is_invocable_v<Visitors, Value> or
...);
Vous l'appelez avec la variante en premier, suivi des visiteurs. Voici le quickbench de la mise à jour 6 avec celui-ci ajouté . Un lien vers le banc est ici http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw
Donc, avec cela, je pense que la décision de visiter ou non revient à ce qui est plus expressif et plus clair dans son intention. La performance peut être obtenue dans les deux sens.