为什么 std::unique_ptr 没有优化而 std::variant 可以?

Why std::unique_ptr isn't optimized while std::variant can?

我试图比较 std::visitstd::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 多态性?

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

这个更好但仍然不可靠: ver 1, ver 2 with arrays.

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

为如此小的极快功能设置基准测试非常困难且充满陷阱,因此必须格外小心,因为您已达到基准测试工具的局限性。

我已经使用 -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, 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 调用的 side-effect)。因此,编译器无法轻易证明移除循环是安全的,但事实并非如此。

如果从两个 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,所以在 run-time 不需要计算任何东西:变体的最终状态已经可以在 compile-time 确定。不过,现在的不同之处在于,一些其他翻译单元可能希望稍后访问 var 的值,因此必须写入该值。