【问题标题】:Inline virtual function when called from another virtual function?从另一个虚函数调用时内联虚函数?
【发布时间】:2012-10-09 10:42:09
【问题描述】:

我有一个具有两个虚拟成员函数的类:foowrapperfoo 又短又快,wrapper 包含一个循环多次调用foo。我希望有一些方法可以内联对 foo 的调用在包装函数内,即使是从指向对象的指针调用时:

MyClass *obj = getObject();
obj->foo(); // As I understand it, this cannot be inlined. That's okay.
obj->wrapper(); // Nor will this. However, I hope that the machine code
                // for the wrapper function will contain inlined calls to
                // foo().

本质上,我希望编译器生成多个版本的包装函数——每个可能的类一个——并内联调用适当的foo,这应该是可能的,因为之前确定了对象类型选择要执行的wrapper 函数。 这可能吗?有没有编译器支持这种优化?

编辑:我感谢迄今为止的所有反馈和答案,我最终可能会选择其中之一。但是,大多数回复忽略了我问题的最后一部分,我解释了为什么我认为这种优化应该是可行的。这确实是我问题的症结所在,我仍然希望有人能解决这个问题。

编辑 2:我选择了 Vlad 的答案,因为他既提出了流行的解决方法,又部分解决了我提出的优化建议(在 David 的答案中)。感谢所有写答案的人——我都读了,没有一个明确的“赢家”。

另外,我发现一篇学术论文提出了与我的建议非常相似的优化:http://www.ebb.org/bkuhn/articles/cpp-opt.pdf

【问题讨论】:

  • 编译器可能知道足以将虚函数查找优化为一次性命中。
  • 没错,但这仍然会留下(更大?)非内联函数调用的开销,对吧?
  • 是的,但缺少机器代码检测可能无法实现。
  • 为什么?我试图在我的问题的最后一部分解释为什么我认为它应该是可能的——这对你有任何意义吗?
  • 这个也许可以,但很难说。我相当怀疑编译器会生成这样的代码,因为它不能保证在你正在编写的类的其他子类不存在的情况下工作。

标签: c++ gcc inline virtual-functions


【解决方案1】:

想象以下层次结构:

class Base
{
    virtual void foo();
    virtual void wrapper();
};

class Derived1: public Base
{
    virtual void foo() { cout << "Derived1::foo"; }
    virtual void wrapper() { foo(); }
};

class Derived2: public Derived1
{
    virtual void foo() { cout << "Derived2::foo"; }
};

Base * p1 = new Derived1;
p1->wrapper();  // calls Derived1::wrapper which calls Derived1::foo
Base * p2 = new Derived2;
p2->wrapper();  // calls Derived1::wrapper which calls Derived2::foo

你能看出问题吗? Derived1::wrapper 必须调用 Derived2::foo。直到运行时它才能知道它是调用 Derived1::foo 还是 Derived2::foo,所以没有办法内联它。

如果您想确保内联是可能的,请确保您要内联的函数不是虚拟的。从您的描述看来,如果层次结构中的每个类都重新实现foowrapper,这可能是可能的。一个函数不需要是虚拟的就可以被覆盖。

【讨论】:

  • 这是真的,但是有一个选项是他希望Derived1::wrapper 调用Derived1::foo,不管它是否是最派生的类。 (虽然不清楚)
  • @Mark:很好的例子。在这种情况下,我希望编译器为我自动生成两个函数:Derived1::wrapper_callFromDerived1 和 Derived1::wrapper_callFromDerived2。然后 Derived1 类型的对象的 vtable 将指向其中的第一个,而 Derived2 类型的对象指向第二个。这有意义吗?
【解决方案2】:

这不准确。 virtual 函数可以内联,但前提是编译器确定地知道对象的静态类型 - 从而保证多态性有效。

例如:

struct A
{
    virtual void foo()
    {
    }
};
struct B
{
    virtual void foo()
    {
    }
};

int main()
{
    A a;
    a.foo(); //this can be inlined
    A* pa = new A;
    pa->foo(); //so can this
}

void goo(A* pa)
{
    pa->foo() //this probably can't
}

也就是说,在您的情况下,这似乎不可能发生。您可以做的是拥有另一个非virtual 函数,该函数实际实现该功能并静态调用它,因此调用在编译时得到解决:

class MyClass
{
    virtual void foo() = 0;
    virtual void wrapper() = 0;
};

class Derived : MyClass
{
    void fooImpl()
    {
       //keep the actual implementation here
    }
    virtual void foo()
    {
       fooImpl();
    }
    virtual void wrapper()
    {
       for ( int i = 0 ; i < manyTimes ; i++ )
          fooImpl(); //this can get inlined
    }
};

或者只是@avakar 指出的Derived::foo()

【讨论】:

  • 请注意,您可以在没有动态调度的情况下调用虚函数:Derived::foo()。这应该消除对fooImpl 的需要。
  • 直到运行时我才知道类型,所以 Derived::foo() 不是一个选项。 fooImpl() 模式似乎是一个合理的解决方法。
  • @DrewFrank 真的是一回事。你从Derived::wrapper 调用Derived::foo,而不是来自同一个班级,所以它可以工作。
  • 啊,我明白了。谢谢!
【解决方案3】:

不,如果通过指针或引用执行函数调用(这包括this 指针),则不会内联函数调用。可以创建一个从当前类型扩展而来的新类型,并覆盖 foo 而不会覆盖 wrapper

如果你想让编译器内联你的函数,你必须禁用该调用的虚拟调度:

void Type::wrapper() {
    Type::foo();            // no dynamic dispatch
}

【讨论】:

  • 戴夫,可能。这些天编译器比烤面包机聪明一点:)
  • @Vlad:当函数被编译时,它对整个程序没有可见性,因此它无法知道是否存在覆盖foo(而不是wrapper)的最派生类型。即使对整个程序进行了优化,您也可以从动态库中加载代码,并且可以有更多类型。这是 C++11 中新的 final 上下文非关键字的原因之一。在这种情况下,因为语言保证它可以,但前提是foo 被标记为final(我还没有看到它在任何地方使用过)
  • 是和不是。这取决于符号的可见性,真的。
  • @VladLazarenko: 如何 编译器在为翻译单元生成代码时如何知道是否在不同的翻译单元中,可能在加载的动态库中,可能在手动加载的动态库不会有扩展这个的类型吗?在某些子情况下实现理论上并非不可能,但我想知道有什么编译器可以做到这一点。 (另外我不确定 symbol visibility 是否正确)
  • Dave,优化是在稍后阶段执行的,不是在为翻译单元生成代码时,而是在为可执行文件生成代码时。在那里,编译器可能知道继承方案以及可见性,并执行所有必要的优化。 Vtables 是一回事,它还可以重新排列参数,重新分配寄存器等。自从我研究这些技术以来已经有一段时间了,所以很遗憾不能给你具体的参考。但是您可能会在 clang 和 gcc 内部寻找答案。
【解决方案4】:

在某些情况下,编译器可以在编译时确定虚拟调度行为并执行非虚拟函数调用甚至内联函数。只有当它能够确定您的类是继承链中的“顶部”或者这两个函数没有以其他方式重载时,它才能做到这一点。通常,这是不可能的,尤其是如果您没有为整个程序启用后期优化。

除非您想检查编译器优化的结果,否则最好不要在内循环中使用虚函数。例如,像这样:

class Foo {
  public:
    virtual void foo()
    {
        foo_impl();
    }

    virtual void bar()
    {
        for (int i = 0; i < ∞; ++i) {
            foo_impl();
        }
    }

  private:
    void foo_impl() { /* do some nasty stuff here */ }
};

但是在这种情况下,您显然放弃了有人可能会进来的想法,从您的类继承并抛出他们自己的“foo”实现以由您的“bar”调用。他们基本上将需要重新实现两者。

另一方面,它闻起来有点像过早的优化。现代 CPU 很可能会“锁定”您的循环,预测退出循环并一遍又一遍地执行相同的 µOP,即使您的方法实际上是虚拟的。所以我建议你在花时间优化它之前仔细确定这是一个瓶颈。

【讨论】:

    【解决方案5】:

    你的函数是virtual,它的类型直到运行时才被指出,那么你如何期望编译器将某个类的代码内联到它里面呢?

    在一些类似的情况下,我有 2 个函数:foo 是虚拟函数,foo_impl 是正常函数,将从 foowrapper 调用

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-06
      • 2012-01-28
      • 2010-10-05
      • 1970-01-01
      • 1970-01-01
      • 2018-02-08
      相关资源
      最近更新 更多