为何std::visit性能表现不佳?改用index()结合std::get<>后性能大幅提升的原因探究
为何std::visit性能表现不佳?改用index()结合std::get<>后性能大幅提升的原因探究
这真是个非常实用的性能观察!很多开发者都会疑惑为什么std::visit有时候性能不如手动用index()+std::get<>,咱们结合你的测试代码来拆解背后的原因。
你的测试代码如下:
#include <benchmark/benchmark.h> #include <memory> #include <vector> #include <variant> #include <cmath> namespace variant_shapes { // 圆形实现 - 不使用虚函数 class Circle { private: double radius_; public: explicit Circle(double radius) : radius_(radius) {} double area() const { return M_PI * radius_ * radius_; } double getRadius() const { return radius_; } }; // 矩形实现 - 不使用虚函数 class Rectangle { private: double width_; double height_; public: Rectangle(double width, double height) : width_(width), height_(height) {} double area() const { return width_ * height_; } double getWidth() const { return width_; } double getHeight() const { return height_; } }; } // 添加一个直接使用 if constexpr 的基准测试 static void BM_StaticDispatch(benchmark::State& state) { std::vector<std::variant<variant_shapes::Circle, variant_shapes::Rectangle>> shapes; shapes.reserve(1000); for (int i = 0; i < 1000; ++i) { if (i % 2 == 0) { shapes.emplace_back(variant_shapes::Circle(5.0)); } else { shapes.emplace_back(variant_shapes::Rectangle(4.0, 6.0)); } } auto visitor = [](auto &&arg) -> double { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, variant_shapes::Circle>) { return arg.area(); } else if constexpr (std::is_same_v<T, variant_shapes::Rectangle>) { return arg.area(); } }; for (auto _ : state) { double totalArea = 0.0; for (const auto& s : shapes) { totalArea += std::visit(visitor, s); } benchmark::DoNotOptimize(totalArea); } } BENCHMARK(BM_StaticDispatch); // 使用 index() + std::get<> 的静态分派基准测试 template<typename T> double get_area(const T& shape) { if constexpr (std::is_same_v<T, variant_shapes::Circle>) { return shape.area(); } else if constexpr (std::is_same_v<T, variant_shapes::Rectangle>) { return shape.area(); } return 0.0; // 不应该到达这里 } static void BM_StaticDispatch2(benchmark::State& state) { std::vector<std::variant<variant_shapes::Circle, variant_shapes::Rectangle>> shapes; shapes.reserve(1000); for (int i = 0; i < 1000; ++i) { if (i % 2 == 0) { shapes.emplace_back(variant_shapes::Circle(5.0)); } else { shapes.emplace_back(variant_shapes::Rectangle(4.0, 6.0)); } } for (auto _ : state) { double totalArea = 0.0; for (const auto& s : shapes) { totalArea += s.index() == 0 ? get_area(std::get<0>(s)) : get_area(std::get<1>(s)); } benchmark::DoNotOptimize(totalArea); } } BENCHMARK(BM_StaticDispatch2);
一、std::visit的性能瓶颈在哪里?
std::visit的设计目标是通用、安全,但这也带来了一些性能上的妥协:
- 间接调用开销:
std::visit需要支持任意数量的变体类型,底层通常会生成跳转表(类似虚函数表),或者通过模板元编程生成多路分支。但在某些编译器优化场景下,尤其是变体类型数量较少时,编译器可能无法完全将跳转表优化为直接条件判断,导致运行时需要通过间接跳转调用逻辑——这和虚函数的调用开销几乎一致。 - 内联障碍:你的visitor是通用lambda,
std::visit需要确保它能处理所有变体类型。虽然编译器会尝试内联,但如果变体类型的处理逻辑复杂,或者编译器无法完全追踪调用路径,就可能出现内联失败,带来额外的函数调用开销。
二、index() + std::get<>为什么能接近CRTP的性能?
这种写法本质上是手动触发静态分派,完全绕开了std::visit的通用机制,让编译器在编译期就确定调用路径:
index()的轻量性:std::variant::index()只是读取变体内部存储的整数索引,没有任何计算开销,相当于直接访问成员变量。- 编译期确定的调用路径:当你用
s.index() == 0 ? get_area(std::get<0>(s)) : get_area(std::get<1>(s))时,编译器完全知道std::get<0>(s)返回Circle类型,std::get<1>(s)返回Rectangle类型。结合get_area里的if constexpr,编译器会在编译期完全展开分支,生成直接的Circle::area()和Rectangle::area()调用——没有任何间接跳转,也没有额外函数调用开销,这和CRTP的静态分派逻辑完全一致。 - 更容易被优化:这种手动分支的写法非常直白,编译器能轻松识别并进行内联、循环展开等优化,进一步提升性能。
三、两种写法的适用场景
- 如果你的变体类型数量较多(比如超过3-4种),手动写
index()分支会非常繁琐,这时候std::visit的可读性优势远大于性能差异,推荐使用。 - 如果变体类型数量很少,且对性能要求极高(比如高频调用的热点路径),那么
index()+std::get<>的手动静态分派是更好的选择。 - 另外,新版本编译器(比如GCC 12+、Clang 14+)对
std::visit的优化已经做得很好,在变体类型较少时,性能可能和手动分支相差无几,你可以根据自己的编译环境测试后再做选择。
内容来源于stack exchange
相关产品推荐
相关产品推荐

