【问题标题】:Inlining private and protected virtual function calls内联私有和受保护的虚函数调用
【发布时间】:2015-09-22 01:30:18
【问题描述】:

考虑以下一段 C++ 代码:

class IFoo {
 public:
  virtual void Bar() const = 0;
};

template <typename Derived>
class AbstractFoo : public IFoo {
 public:
  void Bar() const override {
    int i = 0;
    auto derived = static_cast<const Derived *>(this);
    while (derived->ShouldBar(i++)) {
      derived->DoBar();
    }
  }
};

class FooImpl : public AbstractFoo<FooImpl> {
 private:
  bool ShouldBar(int i) const {
    return i < 10;
  }

  void DoBar() const {
    std::cout << "Bar!" << std::endl;
  }

  friend class AbstractFoo<FooImpl>;
};

int main() {
  std::unique_ptr<IFoo> foo(new FooImpl());
  foo->Bar();
}

当然,这是 curiously recurring template pattern 有一点点扭曲:在虚拟方法 Bar 通过接口 IFoo 多态地调度一次之后,对 ShouldBarDoBar 的调用保持静态,甚至可能内联。如果以另一种方式实现(AbstractFoo 是非泛型的,ShouldBarDoBar 私有虚拟方法),每次迭代都会有两个虚拟函数调用。

这种优化机会问题的情况包括迭代方案,例如巨大状态空间的depth-first searchsaturation。在这些算法的某些点上,具体实现必须选择继续搜索的方向、是否将状态添加到结果集等。多态实现,这些可能导致数百万次虚拟调用相对较小的函数(其中一些甚至可能是空的!),它的性能损失甚至可以通过分析来衡量。 (请记住,这些迭代算法通常不执行 I/O,这与上面的玩具示例相反。)

在没有 CRTP 的语言中,唯一的替代解决方案是重复迭代方案的“骨架”。例如,在 C# 中,这并不太痛苦,因为我们有部分方法:

interface IFoo {
  void Bar();
}

// This is copy-pasted for every IFoo implementation.
partial class FooImpl : IFoo {
  void Bar() {
    int i = 0;
    bool shouldBar = false;
    ShouldBar(i++, out shouldBar);
    while (shouldBar) {
      DoBar();
      ShouldBar(i++, out shouldBar);
    }
  }

  partial void ShouldBar(int i, out bool result);

  partial void DoBar();
}

partial class FooImpl {
  partial void ShouldBar(int i, our bool result) {
    result = i < 10;
  }

  partial void DoBar() {
    Console.WriteLine("Bar!");
  }
}

如你所见,还是有一些尴尬,因为部分方法必须返回void,并且需要复制“抽象”类的代码。

是否有任何语言/运行时环境可以对简单的虚拟保护方法执行此优化?

我认为问题归结为虚拟公共方法不应该为每个实现生成机器代码,而是为每个具体类。考虑一个简单的vtable,FooImpl 的vtable 中的插槽不应该在IFoo#Bar 的插槽中保存AbstractFoo#Bar,而是一个专门的FooImpl#Bar,对ShouldBarDoBar 进行非虚拟/内联调用由 JIT 生成。

是否有任何环境能够执行这种优化,或者至少在这个方向上进行一些研究?

【问题讨论】:

  • 既然 C++ 拥有一切所需,为什么还要寻找其他语言?
  • 虽然 C++ 非常适合(几乎)零成本的抽象,但像 CRTP 这样的东西很难被称为直观。此外,分代垃圾收集器的优点是无法用 RAII 轻松模拟(例如,分摊小对象的分配和释放成本)并且需要托管运行时。虽然 perharps,但我最好还是使用 C++/CLI ... :)

标签: inheritance compiler-optimization jit virtual-functions crtp


【解决方案1】:

不要使用 JIT,使用 CPU 的分支预测器。任何体面的 CPU 都会尝试缓存每个间接分支指令的目标,因此正确预测的间接分支的成本与条件分支相同,通常为零。

优化此模式与通常的优化过程没有什么不同。您的分析器应将特定的间接分支指令标记为瓶颈。通过将每条慢速指令分成几个更可预测的指令进行优化,例如

if ( likely_to_be_FooImpl ) {
    foo->Bar();
} else {
    foo->Bar();
}

防止编译器消除明显多余的分支留作练习;)。或者,理想情况下,一个分支根本不需要间接调度:

if ( certain_to_be_FooImpl ) {
    static_cast< FooImpl * >( foo )->fooImpl::Bar();
} else {
    foo->Bar();
}

在任何情况下,JIT 都需要寻找本地程序状态和分支目标之间的相关性。 JIT 可能会注意到一个分支倾向于去某个特定的目的地,但 CPU 已经在硬件中优化了这种情况。相反,只要分支的数量不超过预测器的内存限制,虚假的间接分支就会被预测掉。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 2012-04-03
    • 1970-01-01
    • 2012-05-06
    • 2011-05-27
    • 2023-03-23
    • 2010-12-16
    相关资源
    最近更新 更多