`std::variant` vs. Vererbung vs. andere Wege (Performance)

Lesezeit: 19 Minuten

Benutzer-Avatar
moisrex

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:

Benchmark
http://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg

Sie erhalten ein anderes Ergebnis, wenn Sie die Optimierung aktivieren:

Benchmark mit aktiviertem op

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.
Benchmark
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.
zufälliger Zugriff
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.
leer zufällig hinzugefügt
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 devirtualisieren get. 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


Benutzer-Avatar
darune

std::visit scheint bei einigen Implementierungen noch einige Optimierungen zu fehlen. Abgesehen davon gibt es einen zentralen Punkt, der in diesem laborähnlichen Setup nicht sehr gut zu sehen ist – nämlich, dass variantenbasiertes Design stapelbasiert ist im Vergleich zum virtuellen Vererbungsmuster, das sich natürlich in Richtung Heap-basiert bewegt. In einem realen Szenario bedeutet dies, dass das Speicherlayout sehr wohl fragmentiert werden könnte (vielleicht im Laufe der Zeit – sobald Objekte den Cache verlassen usw.) – es sei denn, es kann irgendwie vermieden werden. Das Gegenteil ist das variantenbasierte Design, das im zusammenhängenden Speicher angeordnet werden kann. Ich glaube, das ist ein extrem wichtig Punkt zu beachten, wenn es um nicht zu unterschätzende Leistung geht.

Um dies zu veranschaulichen, bedenken Sie Folgendes:

std::vector<Base*> runtime_poly_;//risk of fragmentation

vs.

std::vector<my_var_type> cp_time_poly_;//no fragmentation (but padding 'risk')

Diese Fragmentierung ist etwas schwierig in einen Benchmark-Test wie diesen einzubauen. Ob dies (auch) im Zusammenhang mit bjarnes Aussage steht, ist mir nicht klar, als er sagte, es könnte möglicherweise schneller sein (was meiner Meinung nach zutrifft).

Andere sehr wichtig Sache zu erinnern für die std::variant basiertes Design besteht darin, dass die Größe jedes Elements die Größe des größtmöglichen Elements verbraucht. Wenn also Objekte nicht ungefähr die gleiche Größe haben, muss dies sorgfältig überlegt werden, da dies negative Auswirkungen auf den Cache haben kann.

Wenn man diese Punkte zusammen betrachtet, ist es schwer zu sagen, was im allgemeinen Fall am besten zu verwenden ist – es sollte jedoch klar genug sein, wenn das Set ein geschlossenes “kleines” Set von ungefähr der gleichen Größe ist – dann zeigt der Variantenstil großes Potenzial, schneller zu sein (wie bjarne anmerkt).

Wir haben jetzt nur die Leistung betrachtet und es gibt tatsächlich andere Gründe, sich für das eine oder andere Muster zu entscheiden: Am Ende müssen Sie nur den Komfort des „Labors“ verlassen und Ihre realen Anwendungsfälle entwerfen und bewerten.

  • Dies mag eine dumme Frage sein, aber warum schlagen Sie vor? std::vector<Base*> im Beispiel statt std::vector<Base>?

    – luizfls

    21. April 2021 um 6:17 Uhr

  • Ich denke, es ist platzsparend, da es besser ist, einen Vektor von Zeigern zu erstellen, die auf dem Heap zugewiesene Objekte zeigen, anstatt auf Objekte selbst.

    – Sankalp Pandya

    19. Mai 2021 um 9:21 Uhr

  • @luizfls std::vector<Base> ist nicht polymorph

    – darune

    5. August 2021 um 18:06 Uhr


  • @luizfls Lookup “Object Slicing in C++”, um die Verwendung von zu verstehen Base*. Kurz gesagt würde die Verwendung von Base bedeuten, dass alles, was nicht in Base, sondern in seinen abgeleiteten Klassen enthalten ist, “abgeschnitten” wird.

    – Benutzer1556435

    31. August 2021 um 13:26 Uhr

Sie können sie alle mit einer Besuchsimplementierung abgleichen, wenn Sie garantieren können, dass die Variante niemals ausnahmsweise leer ist. Hier ist ein einzelner Besuchsbesucher, der mit dem virtuellen oben übereinstimmt und sehr gut mit jmp-Tabellen verknüpft ist. 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
                                        ...);

Du rufst es zuerst mit der Variante auf, gefolgt von den Besuchern. Hier ist die Quickbench von Update 6 mit hinzugefügt Quickbench-Benchmark, der die Leistung von visit_nt zeigt. Ein Link zur Bank ist hier http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw

Daher denke ich, dass die Entscheidung, ob man einen Besuch abstattet oder nicht, darauf hinausläuft, was ausdrucksstärker und klarer in der Absicht ist. Die Leistung kann so oder so erreicht werden.

Basierend auf Update 6 http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y

Ich denke, wir können die Zeit nicht vergleichen, aber relativ zueinander scheinen die Ergebnisse unterschiedlich genug zu sein, um Auswahlmöglichkeiten in der Bibliotheksimplementierung aufzuzeigen.

  • Visual 2019 v16.8.3

  • cl 19.28.29335 x64

  • in /std:c++17 kompilieren

     Run on (8 X 3411 MHz CPU s)
        CPU Caches:
        L1 Data 32 KiB (x4)
        L1 Instruction 32 KiB (x4)
        L2 Unified 256 KiB (x4)
        L3 Unified 8192 KiB (x1)
    
     -------------------------------------------------------------------
     Benchmark                         Time             CPU   Iterations
     -------------------------------------------------------------------
     TradeSpaceForPerformance       5.41 ns         5.47 ns    100000000
     Virtual                        11.2 ns         10.9 ns     56000000
     FunctionPointerList            13.2 ns         13.1 ns     56000000
     Index                          4.37 ns         4.37 ns    139377778
     GetIf                          4.79 ns         4.87 ns    144516129
     HoldsAlternative               5.08 ns         5.16 ns    100000000
     ConstexprVisitor               4.16 ns         4.14 ns    165925926
     StructVisitor                  4.26 ns         4.24 ns    165925926
     Overload                       4.21 ns         4.24 ns    165925926
    

1012370cookie-check`std::variant` vs. Vererbung vs. andere Wege (Performance)

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy