关于继承:内联私有和受保护的虚函数调用

Inlining private and protected virtual function calls

考虑下面的C ++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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();
}

当然,这是一个奇怪的重复发生的模板模式,但有一点点变化:通过接口IFoo将虚拟方法Bar多态调度一次之后,对ShouldBarDoBar的调用保持静态,甚至可以内联。如果以另一种方式实现(AbstractFoo是非泛型以及ShouldBarDoBar私有虚拟方法),则每次迭代将有两个虚拟函数调用。

这种优化机会的情况包括迭代方案,例如深度优先搜索和巨大状态空间的饱和。在这些算法的某个时刻,具体的实现必须选择继续搜索的方向,是否将状态添加到结果集中等。通过多态实现,这些可能导致对相对较小的函数进行数百万次虚拟调用(其中有些甚至可能是空的!),即使通过分析也可以测量性能。 (请记住,与上面的示例相反,这些迭代算法通常不执行I / O。)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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中的插槽不应将AbstractFoo#Bar容纳在IFoo#Bar的插槽中,而应使用专门的FooImpl#Bar,且必须对ShouldBarDoBar进行非虚拟/内联调用由JIT生成。

是否有能够执行此优化的环境,或者至少在此方向上有一些研究?


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

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

1
2
3
4
5
if ( likely_to_be_FooImpl ) {
    foo->Bar();
} else {
    foo->Bar();
}

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

1
2
3
4
5
if ( certain_to_be_FooImpl ) {
    static_cast< FooImpl * >( foo )->fooImpl::Bar();
} else {
    foo->Bar();
}

无论如何,对于JIT来说,在本地程序状态和分支目标之间寻找相关性是一项艰巨的任务。 JIT可能会注意到分支倾向于去某个特定的目的地,但是CPU已经在硬件中优化了这种情况。相反,只要分支的数量不超过预测变量的内存限制,就会伪造间接分支。