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(); } |
当然,这是一个奇怪的重复发生的模板模式,但有一点点变化:通过接口
这种优化机会的情况包括迭代方案,例如深度优先搜索和巨大状态空间的饱和。在这些算法的某个时刻,具体的实现必须选择继续搜索的方向,是否将状态添加到结果集中等。通过多态实现,这些可能导致对相对较小的函数进行数百万次虚拟调用(其中有些甚至可能是空的!),即使通过分析也可以测量性能。 (请记住,与上面的示例相反,这些迭代算法通常不执行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!"); } } |
如您所见,仍然存在一些尴尬,因为部分方法必须返回
是否有任何语言/运行时环境可以在简单的虚拟受保护方法上执行此优化?
我认为问题归结为以下事实:虚拟公共方法不应在其中为每个实现生成机器代码,而应为每个具体类生成机器代码。考虑一个简单的vtable,
是否有能够执行此优化的环境,或者至少在此方向上有一些研究?
不要使用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已经在硬件中优化了这种情况。相反,只要分支的数量不超过预测变量的内存限制,就会伪造间接分支。