`std :: variant` vs.继承vs.其他方式(性能)

moi*_*rex 35 c++ performance inheritance c++17 std-variant

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

pattern checking解释std::holds_alternativesoverloaded方法之后,Bjarne Stroustrup在“ C ++之旅”中谈到了这一点:

这基本上等效于虚拟函数调用,但可能更快。与所有性能要求一样,当性能至关重要时,应通过测量来验证“可能更快”。对于大多数用途,性能差异不明显。

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

基准 http://quick-bench.com/N35RRw_IFO74ZihFbtMu4BIKCJg

如果启用优化,您将得到不同的结果:

启用op的基准

http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc

这是我用于基准测试的代码;我确信,有更好的方法来实现和使用变体来代替虚拟关键字来使用它们(继承与std :: variant):

删除旧代码;看看更新

任何人都可以解释一下实现此用例的最佳方法是什么,这std::variant使我不得不进行测试和基准测试:

我当前正在实现RFC 3986,它是“ URI”,在我的用例中,该类将更多地用作const,并且可能不会做很多更改,并且用户更有可能使用此类来查找每个特定类URI的一部分,而不是生成URI;因此,有意义的是使用std::string_viewURI的每个段,而不是将其分开std::string。问题是我需要为其实现两个类。当我只需要一个const版本时;另一个用于当用户想要创建URI而不是提供一个URI并进行搜索时。

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

您认为我该怎么办?


更新(2)

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

我对try-catch hell的结果感到惊讶,但是由于有了现在有意义的注释

更新(3)

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

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

更新(4)

我消除了生成随机数的开销(我在上一次更新中已经做到了,但是似乎我抓取了错误的URL作为基准),并添加了EmptyRandom来理解生成随机数的基准。并且对Virtual进行了一些小的更改,但我认为它没有任何影响。 空随机添加 http://quick-bench.com/EmhM-S-xoA0LABYK6yrMyBb8UeI

http://quick-bench.com/5hBZprSRIRGuDaBZ_wj0cOwnNhw(已删除了“虚拟”,因此您可以更好地比较其余虚拟)


更新(5)

正如豪尔赫·贝隆(Jorge Bellon)在评论中说的那样,我不是在考虑分配成本。所以我将每个基准转换为使用指针。这种间接影响当然会影响性能,但是现在更加公平了。因此,现在循环中没有分配。

这是代码:

删除旧代码;看看更新

到目前为止,我已经运行了一些基准测试。看来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
Run Code Online (Sandbox Code Playgroud)

对于c:

-------------------------------------------------------------------
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
Run Code Online (Sandbox Code Playgroud)

现在,对于铛,这是更好地使用虚拟继承但对于G ++这是更好地使用holds_alternativeget_if但总体而言,std::visit似乎不是几乎所有的基准迄今一个不错的选择。

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

我想知道package.index()结果。不应该更快吗?它有什么作用?

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

使用One one而不是auto one = new One基于Maxim Egorushkin的注释的版本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

-------------------------------------------------------------------
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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

lang编译器的结果(对此我感到惊讶):

-------------------------------------------------------------------
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
Run Code Online (Sandbox Code Playgroud)

迄今为止最好的基准测试(将更新):

dar*_*une 13

std::visit在某些实现上似乎还缺少一些优化。话虽如此,在这个类似实验室的设置中,有一个中心点并不是很明显——即基于的设计是基于堆栈的,而虚拟模式自然会倾向于基于堆。在现实世界中,这意味着内存布局很可能会碎片化(可能随着时间的推移 - 一旦对象离开缓存等) - 除非可以以某种方式避免。相反的是可以在连续内存中布局的基于的设计。我相信这是一个非常重要的考虑点,当涉及到不能低估的性能时。

为了说明这一点,请考虑以下事项:

std::vector<Base*> runtime_poly_;//risk of fragmentation
Run Code Online (Sandbox Code Playgroud)

对比

std::vector<my_var_type> cp_time_poly_;//no fragmentation (but padding 'risk')
Run Code Online (Sandbox Code Playgroud)

这种碎片化有点难以构建到像这样的基准测试中。如果这(也)在 bjarne 的声明的上下文中,我不清楚他说它可能会更快(我相信这是真的)。

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

综合考虑这些点,很难说在一般情况下哪个最好使用 - 但是如果集合是一个封闭的“小”集合,大小大致相同,那么应该足够清楚 - 然后变体风格显示出更快的巨大潜力(作为 bjarne 笔记)。

我们现在只考虑性能,选择一种或另一种模式确实有其他原因:最后,您只需要摆脱“实验室”的舒适感,设计您的真实用例并对其进行基准测试。

  • @luizfls `std::vector&lt;Base&gt;` 不是多态的 (3认同)

Bea*_*hed 7

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

首先用变体来调用它,然后用访问者来调用它。这是更新 6 快速台,添加了它显示 Visit_nt 性能的 Quickbench 基准测试。长凳的链接在这里http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw

因此,我认为是否访问的决定取决于什么更具表现力和意图更明确。无论哪种方式都可以实现性能。