18

希望这对于 StackOverflow 来说不是太专业的问题:如果它是并且可以迁移到其他地方,请告诉我......

很多个月前,我写了一篇本科论文,提出了 C++ 和相关语言的各种去虚拟化技术,通常基于代码路径的预编译特化(有点像模板)的想法,但在运行时选择正确的特化进行检查,以防出现以下情况它们不能在编译时选择(因为必须是模板)。

(非常)基本的想法类似于以下内容......假设您有一个C类似以下的类:

class C : public SomeInterface
{
public:
    C(Foo * f) : _f(f) { }

    virtual void quack()
    {
        _f->bark();
    }

    virtual void moo()
    {
        quack(); // a virtual call on this because quack() might be overloaded
    }

    // lots more virtual functions that call virtual functions on *_f or this

private:
    Foo * const _f; // technically doesn't have to be const explicitly
                    // as long as it can be proven not be modified
};

并且您知道存在具有已知完整类型的Foolike FooAFooB等的具体子类(不一定有详尽的列表),然后您可以C为某些选定的子类预编译 的专用版本Foo,例如(请注意,构造函数不是故意包括在这里,因为它不会被调用):

class C_FooA final : public SomeInterface
{
public:
    virtual void quack() final
    {
        _f->FooA::bark(); // non-polymorphic, statically bound
    }

    virtual void moo() final
    {
        C_FooA::quack(); // also static, because C_FooA is final
        // _f->FooA::bark(); // or you could even do this instead
    }

    // more virtual functions all specialized for FooA (*_f) and C_FooA (this)

private:
    FooA * const _f;
};

并将构造函数替换为C以下内容:

C::C(Foo * f) : _f(f)
{
    if(f->vptr == vtable_of_FooA) // obviously not Standard C++
        this->vptr = vtable_of_C_FooA; 
    else if(f->vptr == vtable_of_FooB)
        this->vptr = vtable_of_C_FooB;
    // otherwise leave vptr unchanged for all other values of f->vptr
}

所以基本上,正在构造的对象的动态类型会根据其构造函数的参数的动态类型而改变。(请注意,您不能使用模板执行此操作,因为您只能在编译时C<Foo>知道类型的情况下创建 a)。f从现在开始,任何对FooA::bark()through的调用C::quack()都只涉及一个虚拟调用:或者调用C::quack()静态绑定到动态调用的非专业版本FooA::bark(),或者调用C::quack()动态转发到C_FooA::quack()静态调用FooA::bark()。此外,如果流分析器有足够的信息进行静态调用,动态调度可能会在某些情况下完全消除。C_FooA::quack(),如果它允许内联,这在紧密循环中可能非常有用。(虽然从技术上讲,即使没有这种优化,你也可能没问题......)

(请注意,这种转换是安全的,尽管用处不大,即使_f是非常量且受保护而不是私有的并且C是从不同的翻译单元继承的……为继承的类创建 vtable 的翻译单元在关于继承类的特化和构造函数的所有内容都将设置this->vptr为它自己的 vtable,它不会引用任何专门的函数,因为它不知道任何关于它们的信息。)

这似乎需要付出很多努力来消除一个间接级别,但关键是您可以仅基于内部的本地信息对任何任意嵌套级别(遵循此模式的虚拟调用的任何深度都可以减少到一个)执行此操作一个翻译单元,并以一种有弹性的方式执行它,即使在您不知道的其他翻译单元中定义了新类型......您可能会添加很多您不会有的代码膨胀,否则如果您天真地做了。

无论如何,不​​管这种优化是否真的有足够的物有所值,值得努力实现,也值得在生成的可执行文件中占用空间,我的问题是,标准 C++ 中是否有任何东西可以防止执行这种转换的编译器?

我的感觉是否定的,因为该标准根本没有指定如何完成虚拟调度或如何表示指向成员函数的指针。我很确定 RTTI 机制没有任何内容可以防止CC_FooA伪装成相同类型的所有用途,即使它们有不同的虚拟表。我能想到的唯一可能重要的另一件事是仔细阅读 ODR,但可能不是。

我忽略了什么吗?除非 ABI/链接问题,这样的转换是否可以在不破坏符合 C++ 程序的情况下进行?(此外,如果是的话,目前是否可以使用 Itanium 和/或 MSVC ABI 来完成?我很确定答案也是肯定的,但希望有人可以确认。)

编辑:有谁知道在 C++、Java 或 C# 的任何主流编译器/JIT 中是否实现了类似的东西?(请参阅下面评论中的讨论和链接聊天...)我知道 JIT 直接在呼叫站点进行虚拟的推测性静态绑定/内联,但我不知道他们是否做了类似的事情(使用全新的 vtables基于在构造函数而不是在每个调用站点完成的单个类型检查来生成和选择)。

4

2 回答 2

1

标准 C++ 中有什么东西会阻止编译器执行这种转换吗?

如果您确定可观察的行为没有改变,那就不是 - 这就是标准第 1.9 节中的“假设规则”。

但这可能会使证明您的转换是正确的非常困难:12.7/4:

当从构造函数(包括非静态数据成员的mem-initializer大括号或等式初始化器)或从析构函数直接或间接调用虚函数时,调用适用的对象是对象在构造或析构中,调用的函数是在构造函数或析构函数自己的类或其基类之一中定义的函数,但不是在派生自构造函数或析构函数自己的类的类中覆盖它的函数,或在其中之一中覆盖它的函数最派生对象的其他基类。

因此,如果析构函数Foo::~Foo()碰巧直接或间接调用C::quack()了一个对象cc._f指向被销毁的对象,你需要调用Foo::bark(),即使_fFooA在你构造对象的时候c

于 2013-03-01T17:32:53.783 回答
0

初读时,这听起来像是多态内联缓存的以 c++ 为中心的变体。我认为 V8 和 Oracle 的 JVM 都使用它,而且我知道.NET 使用.

回答您最初的问题:我认为标准中没有任何内容禁止此类实现。C++ 非常重视“原样”规则。只要你忠实地实现了正确的语义,你就可以用任何你喜欢的疯狂方式来实现。c++ 虚拟调用不是很复杂,所以我怀疑你是否会遇到任何边缘情况(如果你试图用静态绑定做一些聪明的事情)。

于 2013-03-01T15:45:27.557 回答