变体与继承与其他方式(性能)

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

在“ C + + 之旅”的比雅尼·斯特劳斯特鲁普中,在解释了 std::holds_alternativesoverloaded的方法之后,这样描述了 pattern checking:

这基本上等效于虚函数调用,但可能更快 性能时,应该通过测量来验证这个“潜在的更快” 对于大多数应用来说,性能上的差异是微不足道的。

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

benchmark Http://quick-bench.com/n35rrw_ifo74zihfbtmu4bikcjg

如果你打开优化,你会得到一个不同的结果:

benchmark with op enabled

Http://quick-bench.com/p6kiutrxzdhjeifigi8gjboumoc

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

删除了旧代码; 看看更新

有人能解释一下什么是实现 std::variant用例的最佳方法吗? 这个用例让我进行了测试和基准测试:

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

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

你觉得我该怎么做?


更新(二)

谢谢@gan _ 在我的基准代码中提到并修复了提升问题。 benchmark Http://quick-bench.com/mcclomh03nu8ndcgt3t302xknxy

我很惊讶的结果,试图赶上地狱,但感谢 此评论现在是有意义的。

更新(三)

我删除了 try-catch方法,因为它真的很糟糕; 还随机改变了选择的值,看起来,我看到了更现实的基准。看起来 virtual并不是正确的答案。 random access Http://quick-bench.com/o92yrt0tmqtdcvufmipu_fifht0

Http://quick-bench.com/ffbe3bsipdfsmgkfm94xgnfkvks (没有内存泄漏 lol)

更新(四)

我删除了生成随机数的开销(我在上一次更新中已经这样做了,但似乎我抓取了错误的基准 URL) ,并添加了一个 EmptyRandom 来理解生成随机数的基线。并且在虚拟中做了一些小的改变,但是我不认为它会影响任何东西。 empty random added Http://quick-bench.com/emhm-s-xoa0labyk6yrmybb8uei

Http://quick-bench.com/5hbzprsrirgudabz_wj0cownnhw (删除虚拟部分,以便更好地比较其他部分)


更新(五)

正如 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_alternative或者 get_if,但是总的来说,到目前为止,对于我的几乎所有基准测试来说,std::visit似乎都不是一个好的选择。

我认为,如果在 c + + 中加入模式匹配(switch 语句可以检查比整型文字更多的内容) ,那么我们的代码将会更清晰、更易于维护。

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

Clang 版本: http://quick-bench.com/cl0HFmUes2GCSE1w04qt4Rqj6aI

使用 One one而不是基于 Maxim Egorushkin 的评论auto one = new One的版本: http://quick-bench.com/KAeT00__i2zbmpmUHDutAfiD6-Q(不会改变很多结果)


更新(六)

我做了一些改变,结果是非常不同的编译器,现在编译器。但似乎 std::get_ifstd::holds_alternatives是最好的解决方案。virtual似乎最好的工作原因不明与叮当现在。这真的让我很惊讶,因为我记得 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 (亦可参阅海湾合作委员会)

15987 次浏览

std::visit在某些实现上似乎还缺乏一些优化。也就是说,在这个类似于实验室的设置中,有一个中心点并不十分明显,那就是基于 的设计是基于堆栈的,而虚拟的 模式则自然而然地倾向于基于堆的设计。在现实场景中,这意味着内存布局可能会非常碎片化(也许随着时间的推移——一旦对象离开缓存等等)——除非可以以某种方式避免。相反的是基于 的设计,可以在邻接内存中布局。我相信这是一个 非常重要要考虑的一点,当性能是不可低估的。

为了说明这一点,考虑以下几点:

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

对。

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

这种碎片有点难以内置到像这样的基准测试中。 如果这是(也)在比亚内的声明的背景下,我不清楚,当他说,它可能会更快(我相信是真的)。

基于 std::variant的设计需要记住的另一件事是,每个元素的大小占用了尽可能大的元素的大小。因此,如果对象没有大致相同的大小,就必须仔细考虑,因为这可能会对缓存造成不良影响。

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

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

如果您可以保证变量不会因异常而为空,那么您可以将它们与访问实现进行匹配。下面是一个访问访问者,它可以很好地匹配上面的虚拟访问者和 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
...);

您首先使用变体调用它,然后是访问者。 这里是更新6快板,它添加了 Alt = “ Quickbench 基准测试,显示了 access _ nt 的性能”> 。链接到这个板凳是这里 http://quick-bench.com/98aSbU0wWUsym0ej-jLy1POmCBw

因此,我认为决定是否参观归根结底是什么更具表现力和意图更明确。这两种方式都可以实现性能。

基于更新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
    

我在这里添加了 AutoVisitConstVisit: https://quick-bench.com/q/qKvbnsqH1MILeQNWg3XpFfS9f3s

    auto res = std::visit([](auto && v) { return v.get(); }, *package);

这是目前为止最短的解决方案。

并将所有随机 init 内容移动到宏中,以提高各种实现的可读性。