0

我试图比较(多态)和虚函数(多态)的开销std::visitstd::variantstd::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多态性?

4

2 回答 2

3

我已经使用 Clang++ 将您的代码编译为 LLVM(没有您的基准测试),并使用-Ofast. 不出所料,这就是你得到的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, align 8, !tbaa !13, !noalias !8
  %5 = load %struct.Base*, %struct.Base** getelementptr inbounds ({ { %struct.Base* } }, { { %struct.Base* } }* @_ZL3ptr, i64 0, i32 0, i32 0), align 8, !tbaa !4
  store i8* %1, i8** bitcast ({ { %struct.Base* } }* @_ZL3ptr to i8**), align 8, !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*, %struct.Base** getelementptr inbounds ({ { %struct.Base* } }, { { %struct.Base* } }* @_ZL3ptr, i64 0, i32 0, i32 0), align 8, !tbaa !4
  %14 = bitcast %struct.Base* %13 to void (%struct.Base*)***
  %15 = load void (%struct.Base*)**, void (%struct.Base*)*** %14, align 8, !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, i64 0, i32 0, i32 1), align 4, !tbaa !16
  store i32 1000000, i32* getelementptr inbounds ({ { %"union.std::__1::__variant_detail::__union", i32 } }, { { %"union.std::__1::__variant_detail::__union", i32 } }* @var, i64 0, i32 0, i32 0, i32 0, i32 0, i32 0), align 4, !tbaa !18
  ret void
}

这再一次不足为奇了。变体只能包含VarDerived,因此不需要在运行时计算任何内容:变体的最终状态已经可以在编译时确定。但是,现在不同的是,其他一些翻译单元可能想要var稍后访问 的值,因此必须写入该值。

于 2020-10-16T13:50:43.573 回答
1
  1. 您的变体只能存储单一类型,因此这与单个常规变量相同(它更像是一个可选变量)。
  2. 您正在运行未启用优化的测试
  3. 结果不受优化器的保护,因此它可能会破坏您的代码。
  4. 您的代码实际上没有利用多态性,一些编译器能够找出只有一个Base类实现并丢弃虚拟调用。

这更好但仍然不值得信赖: ver 1ver 2 with arrays

是的,多态性在紧密循环中使用时可能会很昂贵。

为如此小的极快功能提供基准测试是困难的,而且充满了陷阱,因此必须极其谨慎地处理,因为您达到了基准测试工具的限制。

于 2020-10-16T13:45:49.890 回答