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

新手上路,请多包涵

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

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

这基本上相当于一个虚函数调用,但可能更快。与所有性能声明一样,当性能至关重要时,应该通过测量来验证这种“可能更快”。对于大多数用途,性能差异是微不足道的。

我已经对我想到的一些方法进行了基准测试,结果如下:

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

如果你开启优化,你会得到不同的结果:

启用 op 的基准测试

http://quick-bench.com/p6KIUtRxZdHJeiFiGI8gjbOumoc

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

删除旧代码;看看更新

谁能解释为 std::variant 实现此用例的最佳方法是什么,这让我进行了测试和基准测试:

我目前正在实现 RFC 3986 ,它是“URI”,对于我的用例,这个类将更多地用作 const,可能不会有太大变化,用户更有可能使用这个类来查找每个特定的URI 的一部分,而不是制作一个 URI;因此使用 std::string_view 而不是在其自己的 std::string 中分隔 URI 的每个段是有意义的。问题是我需要为它实现两个类;当我只需要一个 const 版本时;另一个用于当用户想要创建 URI 而不是提供一个并通过它进行搜索时。

所以我用了 template 来修复它本身的问题;但后来我意识到我可以使用 std::variant<std::string, std::string_view> (或者可能 std::variant<CustomStructHoldingAllThePieces, std::string_view> );所以我开始研究看看它是否真的有助于使用变体。从这些结果来看,似乎使用继承和 virtual 是我最好的选择,如果我不想实现两个不同的 const_uriuri 类。

你觉得我应该怎么做?


更新 (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

对于铿锵声:

 -------------------------------------------------------------------
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 似乎不是到目前为止,我几乎所有的基准测试都是不错的选择。

我认为如果将模式匹配(能够检查除整数文字之外的更多内容的 switch 语句)添加到 c++ 中,那将是一个好主意,我们将编写更清晰、更可维护的代码。

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

Clang 版本: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

 #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)

原文由 The Moisrex 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.2k
1 个回答

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

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

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

对比

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

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

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

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

我们现在只考虑了性能,选择其中一种模式确实有其他原因: 最后,您只需要走出舒适的“实验室”,设计和基准测试您的真实世界用例。

原文由 darune 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏