Je m'interroge sur std::variant
performance. 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
y el overloaded
méthodes :
C'est fondamentalement équivalent à un appel de fonction virtuelle, mais potentiellement plus rapide. Comme pour toutes les revendications de performance, ce ''potentiellement plus rapide'' doit être vérifié par des mesures lorsque la performance est critiques. Pour la plupart des utilisations, la différence de performance est insignifiante.
J'ai testé quelques 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 y a une meilleure façon d'implémenter et d'utiliser les variantes pour les utiliser à la place des mots-clés virtuels ( héritage vs. std::variant ):
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
ce qui m'a amené à faire des tests et des analyses comparatives :
Je suis actuellement en train de mettre en œuvre RFC 3986 qui est 'URI' et pour mon cas d'utilisation, cette classe sera plus utilisée comme une constante et ne sera probablement pas beaucoup modifiée. Il est plus probable que l'utilisateur utilise cette classe pour trouver chaque portion spécifique de l'URI plutôt que de créer un URI ; il était donc logique d'utiliser la classe std::string_view
et 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 ; une pour quand je n'ai besoin que d'une version const ; et une autre pour quand l'utilisateur veut créer l'URI plutôt que d'en fournir un et de le rechercher.
J'ai donc utilisé un template
pour réparer ce qui avait ses propres problèmes ; mais ensuite 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é à faire des recherches pour voir si l'utilisation de variantes est réellement utile ou non. D'après ces résultats, il semble que l'utilisation de l'héritage et des virtual
est mon meilleur choix si je ne veux pas implémenter deux différents const_uri
y uri
classes.
Que pensez-vous que je doive faire ?
Mise à jour (2)
Merci à @gan_ d'avoir mentionné et corrigé le problème de levage dans mon code de référence. http://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY
J'ai été surpris par le résultat de l'enfer try-catch mais grâce à ce commentaire ça a du sens maintenant.
Mise à jour (3)
J'ai retiré le try-catch
J'ai également modifié de manière aléatoire la valeur sélectionnée et, à première vue, j'ai obtenu des résultats plus réalistes. Il semble que virtual
n'est pas la bonne réponse après tout. http://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt0
http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (sans la fuite de mémoire lol)
Mise à jour (4)
J'ai supprimé l'overhead de la 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 pris la mauvaise URL pour le benchmark) et j'ai ajouté un EmptyRandom pour comprendre la ligne de base de la génération de nombres aléatoires. J'ai également effectué quelques petits changements dans Virtual, mais je ne pense pas que cela ait eu une incidence sur quoi que ce soit. http://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI
http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw (j'ai enlevé le virtuel pour que vous puissiez mieux comparer les autres)
Mise à jour (5)
comme Jorge Bellon a déclaré dans les commentaires, je n'avais pas pensé au coût de l'allocation ; j'ai donc converti chaque benchmark pour utiliser des pointeurs. Cette indirection a un impact sur les performances bien sûr, mais c'est plus juste maintenant. Donc, pour l'instant, il n'y a pas d'allocation dans les boucles.
Voici le code :
supprimé l'ancien code ; regardez les mises à jour
J'ai effectué quelques benchmarks 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 le 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
Actuellement, pour clang, il est préférable d'utiliser l'héritage virtuel mais pour g++ il est préférable d'utiliser holds_alternative
o get_if
mais dans l'ensemble, std::visit
ne semble pas être un bon choix pour presque tous mes benchmarks jusqu'à présent.
Je pense que ce serait une bonne idée d'ajouter le pattern matching (des instructions switch capables de vérifier plus de choses que de simples littéraux entiers) au c++, nous écririons un code plus propre et plus facile à maintenir.
Je m'interroge sur le package.index()
résultats. Ne devrait-il pas être plus rapide ? Que fait-il ?
Version de Clang : http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI
La version qui utilise One one
au lieu de auto one = new One
sur la base de Commentaire de Maxim Egorushkin : http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q (ne changeant pas beaucoup le résultat)
Mise à jour (6)
J'ai fait quelques changements et les résultats sont maintenant très différents d'un compilateur à l'autre. Mais il semble que std::get_if
y std::holds_alternatives
sont les meilleures solutions. virtual
semble fonctionner mieux pour des raisons inconnues avec clang maintenant. Cela me surprend vraiment car je me souviens virtual
étant mieux dans gcc. Et aussi std::visit
est totalement hors compétition ; dans ce dernier benchmark, il est même pire que vtable lookup.
Voici le benchmark (exécuté 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 (qui me surprend) :
-------------------------------------------------------------------
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 à ce jour (sera mis à jour) : http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y (consultez également le CCG)