为何动态多态比std::variant更快?测试结果存疑求解
我原本认为,动态多态会因为虚表查找、缓存缺失以及基类指针的内存分配问题,性能弱于静态多态(比如用std::variant实现的)。于是我编写了测试用例对比两者性能,但测试结果却和预期相反——无论是否开启编译器优化,std::variant的性能要么和动态多态相当,要么更差。这是为什么?难道创建lambda的开销真的这么大?
动态多态的接口与派生类
struct IValue { virtual double GetValue(double) const = 0; virtual ~IValue() = default; }; struct DerivedFirst : public IValue { double GetValue(double p) const override { return std::sin(p); } }; struct DerivedSecond : public IValue { double GetValue(double p) const override { return std::cos(p); } };
静态多态的无虚函数类
struct First { double GetValue(double p) const { return std::sin(p); } }; struct Second { double GetValue(double p) const { return std::cos(p); } };
测试函数实现
动态多态测试函数
void TestPol() { std::vector<std::unique_ptr<IValue>> pol; for (int i = 0; i < 10'000; ++i) { pol.push_back(std::make_unique<DerivedFirst>()); pol.push_back(std::make_unique<DerivedSecond>()); } int res = 0; for (double p = 0.0; p <= 1000.0; p += 1.0) { for (const auto& el: pol) { res += el->GetValue(p); } } }
第一种std::variant测试函数
template<class... Ts> struct Overloaded : Ts... { using Ts::operator()...; }; template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>; void TestVar() { std::vector<std::variant<First, Second>> var; for (int i = 0; i < 10'000; ++i) { var.push_back(First()); var.push_back(Second()); } int res = 0; for (double p = 0.0; p <= 1000.0; p += 1.0) { for (const auto& v: var) { res += std::visit(Overloaded{ [p] (const First& f) { return f.GetValue(p); }, [p] (const Second& s) { return s.GetValue(p); } }, v); } } }
第二种std::variant测试函数(返回值处理不同)
void TestVar() { std::vector<std::variant<First, Second>> var; for (int i = 0; i < 10'000; ++i) { var.push_back(First()); var.push_back(Second()); } int res = 0; for (double p = 0.0; p <= 1000.0; p += 1.0) { for (const auto& v: var) { std::visit(Overloaded{ [&res, p] (const First& f) { res += f.GetValue(p); }, [&res, p] (const Second& s) { res += s.GetValue(p); } }, v); } } }
核心计算开销主导:测试中
std::sin和std::cos属于高耗时的数学运算,它们的执行开销远大于动态多态的虚函数调用或std::variant的类型分支判断。整体性能被三角函数计算完全主导,两种多态方式的差异被彻底掩盖。编译器的去虚拟化优化:现代编译器在开启优化后,会对可预测的虚函数调用做去虚拟化处理。你的测试中,
DerivedFirst和DerivedSecond的实例是按固定顺序添加到vector的,编译器可以推断出每个指针的实际类型,直接将虚函数调用替换为普通函数调用,完全消除了动态多态的开销。std::visit的额外开销:虽然
std::variant是静态分发,但std::visit的实现需要处理类型分支逻辑,再加上你在循环内每次创建Overloaded和lambda对象(即便编译器会优化部分,但仍可能产生额外指令开销),这部分开销在核心计算占比低的场景下不明显,但在当前测试中,核心计算已经占满CPU,这部分额外开销会让std::variant的表现不如被优化后的动态多态。缓存友好性未体现差异:
std::variant的对象直接存储在vector中,而动态多态的vector存储的是指向堆对象的指针。但你的测试中,堆对象是连续创建的,内存布局较为连续,缓存缺失的问题并不严重,所以动态多态的缓存劣势没有显现。如果vector中的对象类型是随机分布的,缓存缺失的影响才会凸显出来。
内容的提问来源于stack exchange,提问作者OHtuzh

