web-dev-qa-db-ja.com

`std :: variant`と継承とその他の方法(パフォーマンス)

_std::variant_のパフォーマンスについて疑問に思っています。いつ使用しないのですか?仮想関数は、_std::visit_を使用するよりもはるかに優れているようです。

「C++ツアー」では、Bjarne Stroustrupが_pattern checking_とoverloadedメソッドを説明した後、_std::holds_alternatives_についてこれを述べています。

これは基本的に仮想関数呼び出しと同等ですが、潜在的に高速です。パフォーマンスに関するすべての主張と同様に、この「潜在的に高速」は、パフォーマンスが重要な場合に測定によって検証する必要があります。ほとんどの用途では、パフォーマンスの違いはわずかです。

私は頭に浮かんだいくつかの方法をベンチマークしました、そしてこれらは結果です:

benchmarkhttp://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg

最適化をオンにすると、異なる結果が得られます。

benchmark with op enabled

http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc

以下は、ベンチマークに使用したコードです。仮想キーワードの代わりにそれらを使用するためのバリアントを実装および使用するより良い方法があると確信しています( inheritance vs. std :: variant ):

古いコードを削除しました。アップデートを見てください

私がテストとベンチマークを行った_std::variant_のこのユースケースを実装する最良の方法は誰でも説明できますか?

私は現在実装しています RFC 3986 これは「URI」であり、私のユースケースでは、このクラスはconstとしてより多く使用され、おそらくあまり変更されず、ユーザーがこのクラスを使用して、URIを作成するのではなく、URIの特定の部分をそれぞれ検索します。したがって、_std::string_view_を使用し、URIの各セグメントを独自の_std::string_で区切らないことは理にかなっています。問題は、そのために2つのクラスを実装する必要があったことです。 1つはconstバージョンだけが必要な場合です。もう1つは、ユーザーがURIを提供して検索するのではなく、ユーザーがURIを作成する場合に使用します。

したがって、私はtemplateを使用して、独自の問題があった問題を修正しました。しかし、私は_std::variant<std::string, std::string_view>_(または_std::variant<CustomStructHoldingAllThePieces, std::string_view>_)を使用できることに気付きました。だから私はそれが実際にバリアントを使用するのに役立つかどうかを調べるために研究を始めました。これらの結果から、継承を使用しているようで、2つの異なる_const_uri_およびvirtualクラスを実装したくない場合は、uriが最善の策です。

私はどうしたらいいと思いますか?


アップデート(2)

ベンチマークコードの巻き上げ問題について言及して修正してくれた@gan_に感謝します。 benchmarkhttp://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY

Try-catch hellの結果には驚きましたが、 this comment のおかげで今では意味がわかります。

アップデート(3)

_try-catch_メソッドは本当に悪かったので削除しました。また、選択した値をランダムに変更し、その外観から、より現実的なベンチマークを確認しました。結局、virtualは正解ではないようです。 random accesshttp://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt

http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs (メモリリークなし)

アップデート(4)

乱数を生成するオーバーヘッドを取り除きました(前回の更新ですでにそれを行いましたが、ベンチマーク用に間違ったURLを取得したようです)、乱数を生成するベースラインを理解するためにEmptyRandomを追加しました。また、Virtualにいくつかの小さな変更を加えましたが、影響はないと思います。 empty random addedhttp://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI

http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw (仮想を削除したので、残りを比較しやすくなりました)


アップデート(5)

jorge Bellon said のコメントで、割り当てのコストについては考えていませんでした。そこで、すべてのベンチマークをポインターを使用するように変換しました。この間接性はもちろんパフォーマンスに影響を与えますが、今ではより公平です。したがって、現時点ではループに割り当てはありません。

これがコードです:

古いコードを削除しました。アップデートを見てください

これまでにいくつかのベンチマークを実行しました。 g ++はコードを最適化するより良い仕事をしているようです:

_-------------------------------------------------------------------
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
_

そして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
_

現時点では、clangの場合は仮想継承を使用する方が適切ですが、g ++の場合は_holds_alternative_または_get_if_を使用する方が適切ですが、全体として_std::visit_はほとんどすべての場合に適した選択肢ではないようですこれまでの私のベンチマーク。

パターンマッチング(整数リテラルだけではなく、より多くのものをチェックできるスイッチステートメント)をc ++に追加すると、よりクリーンでメンテナンスしやすいコードを書くことができれば、良い考えだと思います。

package.index()の結果について疑問に思っています。速くないほうがいいですか?それは何をするためのものか?

Clangバージョン: http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI

Maxim Egorushkinのコメント に基づく_One one_ではなく_auto one = new One_を使用するバージョン: http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q (結果をあまり変えない)


アップデート(6)

いくつか変更を加えましたが、結果はコンパイラごとに大きく異なります。しかし、_std::get_if_と_std::holds_alternatives_が最適なソリューションのようです。 virtualは、現在のところ、clangを使用した未知の理由で最適に動作するようです。私はvirtualがgccでより優れていることを覚えているので、それは本当に私を驚かせます。また、_std::visit_は完全に競合外です。この最後のベンチマークでは、vtableルックアップよりもさらに悪いです。

ベンチマークは次のとおりです(GCC/Clangで実行し、libstdc ++と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();
_

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
_

Clangコンパイラの結果(これには驚いています):

_-------------------------------------------------------------------
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
_

これまでの最高のベンチマーク(更新されます): http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y (GCCも確認してください)

38
moisrex

std::visitは、一部の実装ではまだいくつかの最適化を欠いているようです。とは言っても、このラボのような設定ではあまりよく見られない中心点があります。つまり、 バリアント ベースのデザインはスタックベースであるのに対し、仮想 継承 パターンは自然にヒープベースに向けられます。実際のシナリオでは、これはメモリレイアウトが非常によく断片化される可能性があることを意味します(おそらく、オブジェクトがキャッシュを離れるなどして、時間の経過とともに)。反対は variant ベースのデザインであり、メモリに隣接してレイアウトできます。これは、非常に重要なポイントであり、パフォーマンスを低く見積もることができない場合に考慮する必要があります。

これを説明するために、次のことを考慮してください。

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

vs.

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

この断片化は、このようなベンチマークテストに組み込むのがやや困難です。これが(また)bjarneの発言のコンテキスト内にある場合、潜在的に高速になる可能性があると彼が言ったとき、私には明確ではありません(私はそう思うと思います)。

std::variantベースの設計で覚えておくべきもう1つの非常に重要なことは、各要素のサイズが可能な最大の要素のサイズを使い果たすことです。したがって、オブジェクトのサイズがほぼ同じでない場合は、結果としてキャッシュに悪影響を与える可能性があるため、慎重に検討する必要があります。

これらの点を一緒に考えると、一般的なケースでどちらを使用するのが最適かを判断するのは困難ですが、セットがほぼ同じサイズの閉じた「小さめの」ものである場合は十分明確である必要があります。 (bjarneのメモとして)。

今はパフォーマンスのみを考慮しており、実際にいずれかのパターンを選択する理由は他にもあります:結局、「ラボ」と設計の快適さを手に入れなければならないだけです。実世界のユースケースのベンチマーク

6
darune

例外によってバリアントが空にならないことが保証できる場合は、それらをすべて訪問実装と一致させることができます。以下は、上記の仮想に一致し、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
                                        ...);

最初にバリアントで呼び出し、次に訪問者で呼び出します。これが追加されたUpdate 6クイックベンチです Quickbench benchmark showing performance of visit_nt 。ベンチへのリンクはこちら http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw

それで、訪問するかどうかの決定は、より表現的で明確な意図に帰着すると思います。パフォーマンスはどちらの方法でも達成できます。

0
Beached