【问题标题】:Programming Language Idea: Avoiding vtable lookups [closed]编程语言理念:避免 vtable 查找
【发布时间】:2013-01-15 19:19:25
【问题描述】:

我一直在玩弄一种编程语言的想法:它的语法本质上类似于 C++ 和 Java,用于系统编程(或实际上任何需要高性能的编程),但是,在我看来,这是一种比 C++ 更有趣的语法。我正在考虑如何处理分层类结构中的虚拟方法(我的语言不包括多重继承),以及避免 vtable 查找的方法。我的问题是双重的:

  1. 据我了解,vtable 查找如此影响性能的原因(至少在游戏开发等时间紧迫的场景中)是因为它需要引用对象 vtable 指针,并且此 vtable 通常是缓存-错过。这是正确的,还是我遗漏了部分问题?
  2. 我对部分解决方案的想法是:如果编译器可以完全确定对象的类型(即,它不能是从它认为的类型派生的类型),并且该对象作为参数传递给函数其类型是对象类型的超类,则函数中调用的虚方法的位置可以作为一种“隐藏”参数传递,该参数在编译时添加。也许举个例子会有所帮助:

考虑以下类层次结构的伪代码:

class Animal {
    public void talk() { /* Generic animal noise... */ }
    // ...
}

class Dog extends Animal {
    public void talk() { /* Override of Animal::talk(). */ }
    // ...
}

void main() {
    Dog d = new Dog();
    doSomethingWithAnimal(d);
}

void doSomethingWithAnimal(Animal a) {
    // ...
    a.talk();
    // ....
}

请记住,这是伪代码,而不是 C++ 或 Java 或类似代码。此外,假设 Animal 参数是通过引用而不是值隐式传递的。因为编译器可以看到d 肯定是Dog 类型,所以它可以将doSomethingWithAnimal 定义翻译成这样的:

void doSomethingWithAnimal(Animal a, methodptr talk = NULL) {
    // ...
    if ( talk != NULL ) {
        talk(a);
    } else {
        a.talk();
    }
    // ...
}

然后main 看起来会被编译器翻译成这样的东西:

void main() {
    Dog d = new Dog();
    doSomethingWithAnimal(d, Dog::talk);
}

显然,这并不能完全消除对 vtable 的需求,并且可能仍需要为无法确定对象确切类型的情况提供 vtable,但您对此作为性能优化有何看法?我计划尽可能使用寄存器来传递参数,即使参数必须溢出到堆栈上,堆栈上的 methodptr 参数更有可能是缓存命中而不是 vtable 值,对吧?非常感谢任何和所有想法。

【问题讨论】:

  • 这项技术已经在大多数生产 C++ 编译器中实现,并被称为“去虚拟化”。
  • 您可能想重新审视虚拟调度在您测量之前很慢的前提。还要注意if(分支)对性能的影响不同,这可能比虚拟调度的影响更大。当编译器在编译时知道对象类型时,它无论如何都不会使用动态分派,但在您的提议中,函数必须为每个它需要的可能功能。您最终可能会产生比您试图消除的更大的成本......
  • @KyleLutz - 它可以处理 Zach 描述的情况吗?我以为在将doSomethingWithAnimal作为编译单元编译时,编译器无法推断出a的静态类型,因此对手头的问题无能为力。
  • @delnan:问题是我上一条评论中的问题集是否可以在不完全按照编译器现在所做的情况下回答。在问题的一个过于简单的示例中,有一个指向单个成员函数的最终覆盖器的指针,但在实际代码中会有多个这样的成员。将它们作为参数传递会比虚拟调度更昂贵,知道要传递哪些参数在单独的编译模型中会很困难,并且会将函数的实现推送到接口中(至少到编译器看到的接口中)
  • 我非常同意@DavidRodríguez-dribeas:除非您有非常确凿的确凿证据证明虚拟调度是一个关键的性能瓶颈,一直到设计和实施一种新语言是我见过的最极端的反应之一。

标签: c++ compiler-construction programming-languages compiler-optimization vtable


【解决方案1】:

关于 Q1: 缓存利用率实际上只是虚拟调用“问题”的一部分。 virtual 函数和后期绑定的全部意义在于调用站点可以调用任何实现而不需要更改。这需要一些间接性:

  • 间接意味着解决间接的空间和/或时间开销。
  • 无论您如何进行间接调用,如果 CPU 具有良好的分支预测器并且调用站点是单态的(即只有一种实现),间接调用只能与静态调用一样快曾经被称为)。而且我什至不确定在所有硬件开发人员关心的情况下,完美预测的分支是否与静态分支一样快。
  • 在编译时不知道被调用函数也会抑制基于知道被调用函数的优化(内联,还有循环不变的代码运动等等)。

您的方法并没有改变这一点,因此保留了大部分性能问题:它仍然浪费一些时间和空间(仅在额外的参数和分支上,而不是 vtable 查找上),它不允许内联或其他优化,并且不会删除间接调用。

Re 2:这是一种对去虚拟化的跨过程旋转,C++ 编译器已经在某种程度上做到了这一点(在本地,有 @us2012 在 cmets 中描述的限制)。它有一些“小”问题,但如果有选择地应用它可能是值得的。否则,您会生成更多代码,传递 lot 额外参数,执行 lot 额外分支,而只会获得很少甚至净损失。 p>

我认为主要问题是它不能解决上述大多数性能问题。为子类和同一主题的其他变体生成专门的函数(而不是一个通用主体),可能对此有所帮助。但这会产生额外的代码,必须通过性能提升来证明自己是合理的,而且普遍的共识是,即使在性能关键的程序中,这种激进的优化对于大多数代码也是不值得的。

特别是,虚拟调用开销只在相同功能的基准测试中很重要,或者如果您已经从其他所有内容中优化了永远爱的地狱并且需要大量微小的间接调用(游戏开发中的一个示例:每个几何对象的几个虚拟方法调用用于绘图或截锥体剔除)。在大多数代码中,虚拟调用无关紧要,或者至少不足以保证进一步的优化尝试。此外,这仅与 AOT 编译器有关,因为 JIT 编译器有其他方法来处理这些问题。查找多态内联缓存,并注意跟踪 JIT 编译器可以简单地内联 all 调用,无论是否虚拟。

总而言之:vtables 已经是一种快速且通用的实现虚函数的方法(如果它们可以被使用,这里就是这种情况)。您不太可能对它们进行很大改进,更不用说注意到改进了,除非在极少数情况下。如果你想尝试一下,你可以尝试编写一个 LLVM pass 来做这样的事情(尽管你必须在较低级别的抽象上工作)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 2011-01-07
    • 2016-03-06
    • 2012-12-28
    • 2015-08-15
    相关资源
    最近更新 更多