Ich frage mich std::variant
Leistung. Wann sollte ich es nicht verwenden? Es scheint, als wären virtuelle Funktionen immer noch viel besser als die Verwendung std::visit
was mich überrascht hat!
In “Eine Tour durch C++” sagt Bjarne Stroustrup darüber pattern checking
nach dem erklären std::holds_alternatives
und der overloaded
Methoden:
Dies entspricht im Grunde einem virtuellen Funktionsaufruf, ist jedoch potenziell schneller. Wie bei allen Leistungsansprüchen sollte dieses „möglicherweise schneller“ durch Messungen verifiziert werden, wenn die Leistung kritisch ist. Für die meisten Anwendungen ist der Leistungsunterschied unbedeutend.
Ich habe einige Methoden bewertet, die mir in den Sinn gekommen sind, und dies sind die Ergebnisse:
http://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg
Sie erhalten ein anderes Ergebnis, wenn Sie die Optimierung aktivieren:
http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc
Hier ist der Code, den ich für Benchmarks verwendet habe; Ich bin mir sicher, dass es eine bessere Möglichkeit gibt, Varianten zu implementieren und zu verwenden, um sie anstelle von virtuellen Schlüsselwörtern zu verwenden (Vererbung vs. std::variant):
entfernte den alten Code; schau dir die Updates an
Kann jemand erklären, wie man diesen Anwendungsfall am besten umsetzt std::variant
das hat mich zum Testen und Benchmarken gebracht:
Bin gerade am Implementieren RFC 3986 Das ist ‘URI’ und für meinen Anwendungsfall wird diese Klasse eher als Konstante verwendet und wahrscheinlich nicht viel geändert, und es ist wahrscheinlicher, dass der Benutzer diese Klasse verwendet, um jeden bestimmten Teil des URI zu finden, anstatt zu erstellen ein URI; es war also sinnvoll, davon Gebrauch zu machen std::string_view
und nicht jedes Segment des URI in seinem eigenen zu trennen std::string
. Das Problem war, dass ich zwei Klassen dafür implementieren musste; eine für den Fall, dass ich nur eine konstante Version benötige; und eine andere, wenn der Benutzer den URI erstellen möchte, anstatt einen bereitzustellen und zu durchsuchen.
Also benutzte ich eine template
um das zu beheben, was seine eigenen Probleme hatte; aber dann wurde mir klar, dass ich es gebrauchen konnte std::variant<std::string, std::string_view>
(oder vielleicht std::variant<CustomStructHoldingAllThePieces, std::string_view>
); Also begann ich zu recherchieren, um zu sehen, ob es tatsächlich hilft, Varianten zu verwenden oder nicht. Aus diesen Ergebnissen scheint es, als ob Vererbung und verwendet werden virtual
ist meine beste Wahl, wenn ich nicht zwei verschiedene implementieren möchte const_uri
und uri
Klassen.
Was denkst du, sollte ich tun?
Aktualisieren (2)
Danke für @gan_ für die Erwähnung und Behebung des Hubproblems in meinem Benchmark-Code.
http://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY
Ich war überrascht über das Ergebnis von Try-Catch Hell, aber dank dieses Kommentars macht das jetzt Sinn.
Aktualisieren (3)
Ich habe die entfernt try-catch
Methode, da es wirklich schlecht war; und auch zufällig den ausgewählten Wert geändert, und so wie es aussieht, sehe ich einen realistischeren Benchmark. Wie es scheint virtual
ist doch nicht die richtige Antwort.
http://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt0
http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (ohne das Speicherleck lol)
Aktualisieren (4)
Ich habe den Aufwand für die Generierung von Zufallszahlen entfernt (ich habe das bereits im letzten Update getan, aber es scheint, als hätte ich die falsche URL für Benchmarks genommen) und ein EmptyRandom hinzugefügt, um die Grundlinie der Generierung von Zufallszahlen zu verstehen. Ich habe auch einige kleine Änderungen in Virtual vorgenommen, aber ich glaube nicht, dass es irgendetwas beeinflusst hat.
http://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI
http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw (das Virtuelle wurde entfernt, damit Sie den Rest besser vergleichen können)
Aktualisieren (5)
als Jorge Bellon genannt in den Kommentaren habe ich nicht an die Kosten der Zuordnung gedacht; Also habe ich jeden Benchmark konvertiert, um Zeiger zu verwenden. Diese Umleitung wirkt sich natürlich auf die Leistung aus, ist aber jetzt fairer. Im Moment gibt es also keine Zuordnung in den Schleifen.
Hier ist der Code:
entfernte den alten Code; schau dir die Updates an
Ich habe bisher einige Benchmarks laufen lassen. Es scheint, als würde g++ den Code besser optimieren:
-------------------------------------------------------------------
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
Und zum Klingen:
-------------------------------------------------------------------
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
Im Moment ist es für clang besser, virtuelle Vererbung zu verwenden, aber für g ++ ist es besser, sie zu verwenden holds_alternative
oder get_if
aber insgesamt std::visit
scheint bisher für fast alle meine Benchmarks keine gute Wahl zu sein.
Ich denke, es wäre eine gute Idee, wenn Musterabgleich (Switch-Anweisungen, die mehr Dinge als nur Integer-Literale überprüfen können) zu C++ hinzugefügt würden, wir würden saubereren und wartbareren Code schreiben.
Ich wundere mich über die package.index()
Ergebnisse. Sollte es nicht schneller gehen? was tut es?
Clang-Version: http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI
Die verwendete Version One one
anstatt auto one = new One
basierend auf Maxim Egorushkins Kommentar: http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q (Ändert das Ergebnis nicht viel)
Aktualisieren (6)
Ich habe einige Änderungen vorgenommen und die Ergebnisse sind jetzt von Compiler zu Compiler sehr unterschiedlich. Aber es scheint so std::get_if
und std::holds_alternatives
sind die besten Lösungen. virtual
scheint jetzt aus unbekannten Gründen mit Clang am besten zu funktionieren. Das überrascht mich dort wirklich, weil ich mich erinnere virtual
besser sein in gcc. Und auch std::visit
ist völlig außer Konkurrenz; in diesem letzten Benchmark ist es sogar noch schlimmer als die vtable-Suche.
Hier ist der Benchmark (mit GCC/Clang und auch mit libstdc++ und libc++ ausführen):
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();
Ergebnisse für den GCC-Compiler:
-------------------------------------------------------------------
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
Ergebnisse für den Clang-Compiler (was mich überrascht):
-------------------------------------------------------------------
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
Bester Benchmark bisher (wird aktualisiert):
http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y (siehe auch GCC)
“Sie erhalten ein anderes Ergebnis, wenn Sie die Optimierung einschalten”: Das Messen der Leistung in einem nicht optimierten Build ist ziemlich sinnlos.
– Bolov
30. August 2019 um 12:06 Uhr
Ich bin versucht, als “meinungsbasiert” für das Schließen zu stimmen, aber ich werde davon absehen, da in die Frage viel Arbeit gesteckt wird. Hoffe du bekommst eine gute Antwort.
– Bolov
30. August 2019 um 12:08 Uhr
In 5 Ihrer Benchmark-Implementierungen ist der Compiler in der Lage, den Besuch außerhalb der Schleife zu heben. Sie müssen verwenden
DoNotOptimize
an der Variante selbst wenn Sie das Unboxing auf die Bank setzen möchten.– gan_
30. August 2019 um 12:25 Uhr
Dein
Virtual
Benchmark verwendet möglicherweise nicht wirklich dynamisches Dispatch, da der Compiler möglicherweise in der Lage ist, den Aufrufgraphen zu verfolgen und den Aufruf zu devirtualisierenget
. Eine schöne Erklärung finden Sie in dieser Antwort. Mit gcc können Sie das Flag bereitstellen-fno-devirtualize
um diese spezifische Optimierung zu verhindern, ohne die Optimierungen vollständig aufzugeben.– Jonas Greitemann
30. August 2019 um 12:32 Uhr
@moisrex Ein Problem in Ihrem Benchmark-Code ist, dass Sie immer die erste Alternative verwenden (Sie speichern a
One
). Die Try/Catch-Alternative ist grundsätzlich sehr schnell gemacht, wenn die erste Alternative gewählt wird. Wenn ich Ihren Code ändere, um die dritte Alternative (Three
), wird Try/Catch tausendmal langsamer als alle anderen Versionen. quick-bench.com/yIDclCuxfzjzBlCjbVCDNrp13aU– Holt
30. August 2019 um 12:52 Uhr