`std :: variant`与继承以及其他方式(性能)

时间:2019-08-30 12:03:02

标签: c++ performance inheritance c++17 std-variant

我想知道std::variant的性能。什么时候不应该使用它?似乎虚拟函数仍然比使用std::visit更好,这让我感到惊讶!

Bjarne Stroustrup在“ C ++之旅”中对pattern checkingstd::holds_alternatives方法进行了解释之后,对overloaded说道:

  

这基本上等效于虚拟函数调用,但可能更快。与所有的索赔一样   性能时,应通过测量来验证“可能更快”   危急。对于大多数用途而言,性能差异不明显。

我已经确定了一些想到的方法,这些是结果:

benchmark http://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。问题是我需要为其实现两个类。当我只需要一个const版本时;另一个用于当用户想要创建URI而不是提供一个URI并进行搜索时。

因此,我使用template来解决有其自身问题的内容;但是后来我意识到我可以使用std::variant<std::string, std::string_view>(或者也许std::variant<CustomStructHoldingAllThePieces, std::string_view>);所以我开始研究以查看是否确实有助于使用变体。从这些结果来看,如果我不想实现两个不同的virtualconst_uri类,似乎最好使用继承uri

您认为我该怎么办?


更新(2)

感谢@gan_在我的基准代码中提到并解决了起吊问题。 benchmark http://quick-bench.com/Mcclomh03nu8nDCgT3T302xKnXY

我对try-catch hell的结果感到惊讶,但要感谢this comment现在有意义。

更新(3)

我删除了try-catch方法,因为它确实很糟糕;而且还随机更改了所选值,并且通过它的外观,我看到了更现实的基准。看来virtual毕竟不是正确的答案。 random access http://quick-bench.com/o92Yrt0tmqTdcvufmIpu_fIfHt0

http://quick-bench.com/FFbe3bsIpdFsmgKfm94xGNFKVKs(没有内存泄漏大声笑)

更新(4)

我消除了生成随机数的开销(我在上一次更新中已经做到了,但似乎我抓取了错误的URL作为基准),并添加了EmptyRandom来理解生成随机数的基准。并且对Virtual进行了一些小的更改,但我认为它没有任何影响。 empty random added http://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

对于叮当声:

-------------------------------------------------------------------
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_alternativeget_if,但总体而言,std::visit似乎不是一个好选择到目前为止,几乎所有我的基准测试。

我认为,如果将模式匹配(切换语句能够检查不仅仅是整数文字的东西)添加到C ++中,那将是一个好主意,我们将编写更简洁,更可维护的代码。

我想知道package.index()的结果。不应该更快吗?它是做什么的?

C语版本:http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI

基于Maxim Egorushkin's comment使用One one而不是auto one = new One的版本:http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q(对结果的更改不多)


更新(6)

我进行了一些更改,现在每个编译器的结果都大不相同。但是似乎std::get_ifstd::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)

3 个答案:

答案 0 :(得分:3)

std::visit在某些实现上似乎缺少一些优化。话虽这么说,但在这种类似实验室的设置中并没有很好地看到一个中心点-即基于的设计是基于堆栈的,而虚拟模式自然会倾向于基于堆的设计。在现实世界中,这意味着内存布局很可能是碎片化的(也许随着时间的流逝-一旦对象离开缓存等),除非可以避免这种情况。相反的是基于的设计,可以将其布局在连续内存中。我认为这是在考虑性能时不可低估的极其重要点。

为说明这一点,请考虑以下内容:

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

vs。

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

这种碎片很难在像这样的基准测试中建立。 如果这也是bjarne所说的话,我也不清楚bjarne何时说可能会更快(我相信这是对的)。

基于std::variant的设计要记住的另一件事非常重要是,每个元素的大小会占用最大可能元素的大小。因此,如果对象的大小不大致相同,则必须谨慎考虑,因为这样可能会对缓存产生不良影响。

将这些要点综合考虑,很难说在一般情况下哪个是最佳使用-但是,如果集合是大致相同大小的封闭“较小”集合,则应该足够清楚-那么变体样式显示出很大的潜力更快(如bjarne所述)。

我们现在仅考虑性能,确实还有其他原因选择一种或另一种模式:最后,您只需要摆脱“实验室”的麻烦,并设计和基准测试您的真实产品即可。世界用例。

答案 1 :(得分:2)

基于更新 6 http://quick-bench.com/LhdP-9y6CqwGxB-WtDlbG27o_5Y

我想我们无法比较时间,但相对于彼此的结果似乎不同,足以显示库实现中的选择。

  • Visual 2019 v16.8.3

  • cl 19.28.29335 x64

  • 在 /std:c++17 中编译

     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
    

答案 2 :(得分:0)

如果可以保证变体永远不会为空,则可以将它们全部与visit实现匹配。这是一个单一的访问者,它与上面的虚拟匹配,并且与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
                                        ...);

您首先使用变体来调用它,然后是访问者。 这是添加了Quickbench benchmark showing performance of  visit_nt的Update 6 quickbench。 {@ 3}}

在此处链接到工作台

因此,我认为是否访问的决定归结于更具表现力和意图的内容。无论哪种方式都可以实现性能。