内联私有和受保护的虚函数调用
Inlining private and protected virtual function calls
考虑以下 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
多态调度一次后,调用 ShouldBar
和 DoBar
保持静态,甚至可以内联。如果以另一种方式实现,(AbstractFoo
是非泛型的,ShouldBar
和 DoBar
私有虚方法),每次迭代都会有两个虚函数调用 .
这种优化机会问题的情况包括迭代方案,例如巨大状态空间的 depth-first search and saturation。在这些算法的某些点,具体的实现必须选择继续搜索的方向、是否将状态添加到结果集中等。以多态方式实现,这些可能会导致对相对较小的函数进行数百万次虚拟调用(其中一些甚至可能是空的!),这会导致性能损失,甚至可以通过分析来衡量。 (请记住,这些迭代算法通常不执行 I/O,与上面的玩具示例相反。)
在没有 CRTP 的语言中,唯一的替代解决方案是重复 "skeleton" 迭代方案。例如,在 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!");
}
}
可以看到,还是有些别扭,部分方法必须returnvoid
,abstract"class的代码需要重复
是否有任何语言/运行时环境可以对简单的虚拟保护方法执行此优化?
我认为问题归结为 虚拟 public 方法不应该为每个 实现 [=48] 生成机器码=],但对于每个 具体 class。考虑一个简单的 vtable,FooImpl
的 vtable 中的槽不应在 IFoo#Bar
的槽中容纳 AbstractFoo#Bar
,而是具有非虚拟/内联调用的专用 FooImpl#Bar
到由 JIT 生成的 ShouldBar
和 DoBar
。
是否有任何环境能够执行此优化,或者至少在这方面进行了一些研究?
不要使用 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 已经在硬件中优化了这种情况。相反,只要分支数不超过预测器的内存限制,就会预测虚假的间接分支。
考虑以下 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
多态调度一次后,调用 ShouldBar
和 DoBar
保持静态,甚至可以内联。如果以另一种方式实现,(AbstractFoo
是非泛型的,ShouldBar
和 DoBar
私有虚方法),每次迭代都会有两个虚函数调用 .
这种优化机会问题的情况包括迭代方案,例如巨大状态空间的 depth-first search and saturation。在这些算法的某些点,具体的实现必须选择继续搜索的方向、是否将状态添加到结果集中等。以多态方式实现,这些可能会导致对相对较小的函数进行数百万次虚拟调用(其中一些甚至可能是空的!),这会导致性能损失,甚至可以通过分析来衡量。 (请记住,这些迭代算法通常不执行 I/O,与上面的玩具示例相反。)
在没有 CRTP 的语言中,唯一的替代解决方案是重复 "skeleton" 迭代方案。例如,在 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!");
}
}
可以看到,还是有些别扭,部分方法必须returnvoid
,abstract"class的代码需要重复
是否有任何语言/运行时环境可以对简单的虚拟保护方法执行此优化?
我认为问题归结为 虚拟 public 方法不应该为每个 实现 [=48] 生成机器码=],但对于每个 具体 class。考虑一个简单的 vtable,FooImpl
的 vtable 中的槽不应在 IFoo#Bar
的槽中容纳 AbstractFoo#Bar
,而是具有非虚拟/内联调用的专用 FooImpl#Bar
到由 JIT 生成的 ShouldBar
和 DoBar
。
是否有任何环境能够执行此优化,或者至少在这方面进行了一些研究?
不要使用 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 已经在硬件中优化了这种情况。相反,只要分支数不超过预测器的内存限制,就会预测虚假的间接分支。