【问题标题】:Faster inheritance through directly inlining code通过直接内联代码实现更快的继承
【发布时间】:2013-08-16 16:12:36
【问题描述】:

我想出了一种方法(不一定是新方法)来替换 switch 语句的虚拟表。这种方法允许以增加内存为代价内联虚函数。

使用了一种开关,而不是使用表格查找

switch (objecttype)
{
   case objectA: inlined virtual function call for foo from objectA; break;
   case objectB: inlined virtual function call for foo from objectB; break;
   case objectC: inlined virtual function call for foo from objectC; break;
   default: vtable call;
}

所以不是使用指针查找和调用,而是进行比较。可以对已知类进行代码内联。

为了使这项工作顺利进行(避免不仅仅是函数调用),对象需要存储它们的类型。类型需要是连续的。

例如:

class A
{
    ushort objectType; // internal id, say for class A it is 1000
    ushort objectInc; // internal. represents a sort of offset into the jump table
}

class B : A
{
    ushort objectInc; // one more than A's objectInc, has the same objectType
}

etc...

那么switch语句就可以做成一个高效的跳转表比较objectType(确保它是正确的)和使用objectInc和代码大小直接跳转到虚函数的代码(而不是一堆比较)。

据我所知,这个方案的缺点是内存更多(更大的类和更多的内联函数)和更多的编译器复杂性,但虚拟函数可以直接内联(整个 switch 语句可以)所以没有包装调用.由于一些比较和跳转(即O(1)),唯一的额外开销应该只是额外的几个周期。

有没有人对这种方案的性能有任何有用的 cmets 以及为什么不使用它(我敢肯定我不是第一个想到这个的人)?我认为它会相当有效,除非由于比较可能导致缓存失效,但我认为平均而言,对于基类,它可以在直接内联方法调用的几个周期内完成。

顺便说一下,该表可以看作是每个对象派生的内联虚函数调用列表。

假设我们有以下内容:

class A
{
    void foo();
}

class B : A
{
    override void foo();
}

class C : A
{
    override void foo();
}


A a = new C();
a.foo();         // but calls fooWrap


/// internal
void fooWrap(A a)
{
    switch(a.Type)
    {
       case A: a.foo(); break; // A.foo() can be inlined here
       case B: b.foo(); break; // B.foo() can be inlined here
       case C: c.foo(); break; // C.foo() can be inlined here
       default: // use vtable lookup, a's type is not known at compile time
    }
}

(通常fooWrap 将是 vtable 查找)

现在 fooWrap 也可以直接内联,在这种情况下,调用 foo 的成本只是 switch 语句,可以通过使用有效的跳转列表进一步优化。

【问题讨论】:

    标签: c# virtual-functions


    【解决方案1】:

    我会假设这效率较低,因为它需要比较或跳转表或其他任何东西,而通过 vtable 的间接方法调用很快:vtable 中的偏移量可以在调用中硬编码,并且可以使用间接方法调用作为大多数处理器上的直接机器操作。

    此外,每次将另一个后代添加到系统时,您的方法都需要重新编译。因此,对于像 Java 或 .net 这样允许在应用程序运行时甚至从 Internet 加载代码的系统,可能需要在运行时重新编译某些代码。老实说,这样做是为了撤消一些优化,但这只是另一种情况,您必须这样做。

    关于“对象必须存储它们的类型”:在 .net 和 Java 中,情况已经如此:每个对象都包含一个指向其类定义的指针,其中包含 vtable 的其他信息。因此,每个类只有一个 vtable,而不是每个对象。

    【讨论】:

    • 1.跳转表只需要比较来验证对象是否正确,可以假设是正确的,因此通常不采用任何分支。如果调用的是基类,那么它也不需要任何跳转。 2. 不需要重新编译。默认情况下使用 vtable。
    • 比较通常比无条件跳转(间接函数调用要在某种程度上)。
    • @FrankPI Virtual 拨打电话的费用比您暗示的要高。您必须设置变量、推送内容、进行调用、设置堆栈、可能修复变量、返回等... 对于跳转列表,除了默认设置外,没有无条件跳转。可以进行不会导致任何预测问题的间接跳转。也可以内联函数,正如我所描述的那样,这是目的,而虚函数则不能这样做。
    • @user2541029 “设置变量,推送内容,进行调用,设置堆栈,可能修复变量,返回等。”对于每个函数调用都是必需的,无论它是来自虚拟表的间接调用)还是直接函数调用。这只能通过内联或类似的技术(如尾调用优化)来避免,但这又与间接或直接函数调用无关。在编译器可以确定确切的调用类并确定永远不会有后代调用该方法的所有情况下,都可以应用内联。
    • @FrankPI 嗯,对于动态调度,一般不能应用内联。我们不是在谈论特定情况,因为在这些情况下不需要动态调度。事实是,与我描述的方法类似的方法效率更高,并且已经实施(见我的回答)。
    【解决方案2】:

    Smart Eiffel 使用了一种非常相似的描述技术,大约 80% 的虚函数被内联。 There 方法不允许将 vtable 调度作为默认值,这会阻止动态链接,因此这对于通用目的来说可能是一个更合适的想法。

    http://smarteiffel.loria.fr/papers/oopsla97.pdf

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-10-07
      • 2015-08-03
      • 1970-01-01
      • 2019-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-13
      相关资源
      最近更新 更多