web-dev-qa-db-fra.com

`std :: variant` vs héritage vs autres moyens (performances)

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:

benchmarkhttp://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg

Vous obtiendrez un résultat différent si vous activez l'optimisation:

benchmark with op enabled

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?


Mise à jour (2)

Merci pour @gan_ d'avoir mentionné et résolu le problème de levage dans mon code de référence. benchmarkhttp://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.

Mise à jour (3)

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. random accesshttp://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt

http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (sans la fuite de mémoire lol)

Mise à jour (4)

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. empty random addedhttp://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)


Mise à jour (5)

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)


Mise à jour (6)

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)

38
moisrex

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.

6
darune

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é Quickbench benchmark showing performance of visit_nt . 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.

0
Beached