问题描述
我试图比较std::visit
(std::variant
多态性)和虚函数(std::unique_ptr
多态性)的开销。(请注意,我的问题不是关于开销或性能,而是优化。 )
这是我的代码。
https://quick-bench.com/q/pJWzmPlLdpjS5BvrtMb5hUWaPf0
#include <memory>
#include <variant>
struct Base
{
virtual void Process() = 0;
};
struct Derived : public Base
{
void Process() { ++a; }
int a = 0;
};
struct VarDerived
{
void Process() { ++a; }
int a = 0;
};
static std::unique_ptr<Base> ptr;
static std::variant<VarDerived> var;
static void PointerPolyMorphism(benchmark::State& state)
{
ptr = std::make_unique<Derived>();
for (auto _ : state)
{
for(int i = 0; i < 1000000; ++i)
ptr->Process();
}
}
BENCHMARK(PointerPolyMorphism);
static void VariantPolyMorphism(benchmark::State& state)
{
var.emplace<VarDerived>();
for (auto _ : state)
{
for(int i = 0; i < 1000000; ++i)
std::visit([](auto&& x) { x.Process();},var);
}
}
BENCHMARK(VariantPolyMorphism);
我知道这不是一个很好的基准测试,它只是我测试期间的草稿。
但是我对结果感到惊讶。
std::visit
基准测试很高(意味着很慢),没有进行任何优化。
但是,当我打开优化功能(高于O2)时,std::visit
基准测试非常低(这意味着非常快),而std::unique_ptr
则不是。
我想知道为什么不能将相同的优化应用于std::unique_ptr
多态性?
解决方法
我已经用-Ofast
用Clang ++将您的代码编译为LLVM(没有基准测试)。毫无疑问,这是VariantPolyMorphism
的价格:
define void @_Z19VariantPolyMorphismv() local_unnamed_addr #2 {
ret void
}
另一方面,PointerPolyMorphism
确实执行了循环和所有调用:
define void @_Z19PointerPolyMorphismv() local_unnamed_addr #2 personality i32 (...)* @__gxx_personality_v0 {
%1 = tail call dereferenceable(16) i8* @_Znwm(i64 16) #8,!noalias !8
tail call void @llvm.memset.p0i8.i64(i8* nonnull align 16 dereferenceable(16) %1,i8 0,i64 16,i1 false),!noalias !8
%2 = bitcast i8* %1 to i32 (...)***
store i32 (...)** bitcast (i8** getelementptr inbounds ({ [3 x i8*] },{ [3 x i8*] }* @_ZTV7Derived,i64 0,inrange i32 0,i64 2) to i32 (...)**),i32 (...)*** %2,align 8,!tbaa !11,!noalias !8
%3 = getelementptr inbounds i8,i8* %1,i64 8
%4 = bitcast i8* %3 to i32*
store i32 0,i32* %4,!tbaa !13,!noalias !8
%5 = load %struct.Base*,%struct.Base** getelementptr inbounds ({ { %struct.Base* } },{ { %struct.Base* } }* @_ZL3ptr,i32 0,i32 0),!tbaa !4
store i8* %1,i8** bitcast ({ { %struct.Base* } }* @_ZL3ptr to i8**),!tbaa !4
%6 = icmp eq %struct.Base* %5,null
br i1 %6,label %7,label %8
7: ; preds = %8,%0
br label %11
8: ; preds = %0
%9 = bitcast %struct.Base* %5 to i8*
tail call void @_ZdlPv(i8* %9) #7
br label %7
10: ; preds = %11
ret void
11: ; preds = %7,%11
%12 = phi i32 [ %17,%11 ],[ 0,%7 ]
%13 = load %struct.Base*,!tbaa !4
%14 = bitcast %struct.Base* %13 to void (%struct.Base*)***
%15 = load void (%struct.Base*)**,void (%struct.Base*)*** %14,!tbaa !11
%16 = load void (%struct.Base*)*,void (%struct.Base*)** %15,align 8
tail call void %16(%struct.Base* %13)
%17 = add nuw nsw i32 %12,1
%18 = icmp eq i32 %17,1000000
br i1 %18,label %10,label %11
}
原因是两个变量都是静态的。这样,编译器就可以推断出转换单元外部的任何代码都无法访问您的变体实例。因此,您的循环没有任何可见的效果,可以安全地删除。但是,尽管您的智能指针为static
,但它指向的内存仍可能会更改(例如,作为对Process调用的副作用)。因此,编译器无法轻易证明删除循环是安全的,而不能。
如果同时从两个VariantPolyMorphism
中删除静态变量,则会得到:
define void @_Z19VariantPolyMorphismv() local_unnamed_addr #2 {
store i32 0,i32* getelementptr inbounds ({ { %"union.std::__1::__variant_detail::__union",i32 } },{ { %"union.std::__1::__variant_detail::__union",i32 } }* @var,i32 1),align 4,!tbaa !16
store i32 1000000,!tbaa !18
ret void
}
这又不足为奇了。变体只能包含VarDerived
,因此在运行时无需进行任何计算:变体的最终状态已经可以在编译时确定。不过,现在的区别是,其他一些翻译单元可能以后要访问var
的值,因此必须写入该值。
- 您的变体只能存储单一类型,因此与单个常规变量相同(它的工作方式更像是可选变量)。
- 您正在运行测试且未启用优化
- 未从优化程序中保护结果,因此它可能会破坏您的代码。
- 您的代码实际上没有利用多态性,一些编译器能够弄清楚
Base
类只有一种实现并丢弃了虚拟调用。
这更好,但仍然不值得信赖: ver 1,ver 2 with arrays。
是的,在紧密循环中使用多态性可能会很昂贵。
要为如此小的超快速功能制定基准非常困难且充满陷阱,因此,由于达到基准工具的局限性,必须格外小心。