C++:类特化是否是符合编译器的有效转换?

18
我希望这个问题不会太过专业,如果是的话,请告诉我是否需要迁移到其他地方...
很久以前,我写了一篇本科论文,提出了各种针对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
};

如果您知道存在具有已知完整类型(不一定需要详尽列表)的Foo的具体子类,例如FooAFooB等,则可以预编译某些选定的Foo子类的专门版本的C,例如(请注意,构造函数在此处故意未包含,因为它不会被调用):
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
}

基本上,正在构造的对象的动态类型会根据其构造函数的参数的动态类型而改变。(请注意,您无法使用模板执行此操作,因为只有在编译时知道f的类型才能创建C )。从现在开始,通过C :: quack()调用FooA :: bark()的任何调用都只涉及一个虚拟调用:要么对C :: quack()的调用静态地绑定到非专门化版本,该版本动态调用FooA :: bark(),或者对C :: quack()的调用被动态转发到C_FooA :: quack(),后者静态调用FooA :: bark()。此外,在某些情况下,如果流分析器具有足够的信息以对C_FooA :: quack()进行静态调用,则可以完全消除动态调度,如果允许内联,则在紧密循环中非常有用。(尽管在那时,在没有此优化的情况下,您可能已经没问题了...)
(请注意,即使“_f”是非const和受保护的,而不是私有的,并且“C”是从不同的翻译单元继承的,这种转换也是安全的,尽管不太有用...创建继承类的vtable的翻译单元将对特化一无所知,继承类的构造函数将只设置“this->vptr”为其自己的vtable,它不会引用任何特殊的函数,因为它对它们一无所知。)
这可能看起来需要很多工作才能消除一级间接性,但重点是您可以根据翻译单元内的本地信息将其应用于任意嵌套级别(遵循此模式的任何虚拟调用深度都可以减少到一级),并以一种弹性方式执行,即使在其他翻译单元中定义了您不知道的新类型...如果您天真地执行它,您可能会添加许多代码膨胀,否则您将不会有。
无论如何,“独立于这种优化是否真正具有足够的效果,值得实施的努力以及结果可执行文件中的空间开销是否值得”的问题是,标准C ++中是否有任何东西会阻止编译器执行此类转换?
我的感觉是不行的,因为标准并没有指定虚拟分派的实现方式或成员函数指针的表示方式。我相信RTTI机制对于C和C_FooA伪装成同一类型并不会有任何影响,即使它们有不同的虚拟表。唯一可能有影响的是ODR的细节,但可能性很小。
除了ABI/链接问题外,如果不破坏符合C++标准的程序,是否可以进行此类转换?(此外,如果可以,目前能否使用Itanium和/或MSVC ABI进行?我相当确定答案也是肯定的,但希望有人能确认。)
编辑:是否有人知道C++、Java或C#中的任何主流编译器/JIT是否实现了类似的功能?(请参见下面评论中的讨论和链接聊天记录...)我知道JIT在调用点直接进行虚拟静态绑定/内联,但我不知道它们是否像这样做(通过在构造函数中进行单个类型检查来生成全新的虚拟表并进行选择,而不是在每个调用点上进行)。

这不就是将C语言变成一个模板 <typename Foo> 吗? - cooky451
是的,确切地说,您通过SomeInterface进行调用,然后动态获取正确的C_FooX,但是您随后静态调用正确的FooX。或者您静态调用C,它会动态地为您获取正确的FooX。两个虚拟调用而不是一个,并且只要类型信息在当前TU中,您可以将其嵌套到任意层级,并且它对其他TU中添加的附加类型具有弹性(因此您不需要整个程序分析)。 - Stephen Lin
最大的问题是,与模板不同,您在运行时不知道实际需要哪些特化,因此如果盲目进行操作,将会复制大量代码...因此,您需要启发式或程序员提示。 - Stephen Lin
抱歉,在流程分析部分犯了错误,没有仔细查看实现(但是关于获取未知foo的其余评论仍然有效)。 - Stephen Lin
聊天室里的讨论很不错,如果有人想加入的话。 - Stephen Lin
显示剩余10条评论
2个回答

1

有没有任何标准C++中的东西会阻止编译器执行这样的转换?

如果你确定可观测行为不变,那么就没有问题 - 这是标准第1.9节中的“as-if rule”。

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

当从构造函数(包括非静态数据成员的 mem-initializer brace-or-equal-initializer )或析构函数直接或间接调用虚函数,并且调用适用于正在构建或销毁的对象时,被调用的函数是在构造函数或析构函数自己的类中定义的函数或其基类之一,而不是在从构造函数或析构函数自己的类派生的类中覆盖它的函数,或者覆盖它的函数在最派生对象的其他基类中。

因此,如果析构函数 Foo::~Foo() 直接或间接地在对象 c 上调用 C::quack(),其中 c._f 指向正在销毁的对象,则需要调用 Foo::bark(),即使在构造对象 c_f 是一个 FooA


是的,看起来你需要在TU中可用的FooA所有基类的非纯虚析构函数的源代码才能做到这一点,然后只有在你可以证明那里没有对C的任何虚函数的调用时才能这样做。虽然不太方便,但还是可以处理的。无论如何,谢谢,这很有帮助...你还能想到其他什么问题吗? - Stephen Lin
糟糕,你还需要保护一下C的构造函数在FooA子类的构造函数期间被调用的情况...我认为你可以解决这个问题,但是需要控制FooA并提供一些机制,让C能够验证FooA已经完全构建。 - Stephen Lin
“non-pure” 在这里不相关。一个纯虚析构函数有定义,像任何其他函数一样被调用,并且可能导致完全相同的情况。 - aschepler
好的,我不确定当时我在想什么。 - Stephen Lin
实际上,第二种情况也可以,您只需要能够静态分析FooA的构造函数——如果没有FooA的构造函数调用C的构造函数,则派生类型的操作是无关紧要的...我想我很久以前就解决了这个问题,只是忘记了细节。 - Stephen Lin
我会接受这个答案,因为你帮助我发现并修补了这个方法中的漏洞,除非有其他更权威的答案。 - Stephen Lin

0

初次阅读,这听起来像是一个以C++为重点的多态内联缓存变体。我认为V8和Oracle的JVM都在使用它,而且我知道.NET也是如此

回答您最初的问题:我不认为标准中有任何禁止这些实现方式的内容。C++非常严格地遵循“按原样”规则;只要您忠实地实现了正确的语义,就可以以任何疯狂的方式进行实现。C++虚拟调用并不是非常复杂,因此我怀疑您不会在那里遇到任何边缘情况(与如果,例如,您试图对静态绑定进行一些聪明的操作相比)。


据我所知,多态内联缓存只能在调用点直接完成,因此仍然需要每次调用进行守卫检查?(如果该类符合特定模式,则该技术仅在构造函数中进行单个守卫检查,并且之后不会产生任何开销。) - Stephen Lin
是的,但实际上如果你有一个PIC,你也有一个JIT,所以如果你可以静态地证明目标具有特定类型,那么你也可以编译掉分支。但我不确定你是否会费心;只要它们易于预测,分支就很便宜,因此只要类型始终相同,你就赢了。 - David Seiler
不过,这并不总是相同的类型,这就是问题所在。对于单个给定的对象来说,它是相同类型的,但是相同的代码可能会在许多不同的对象之间交替调用,产生不同的目标,破坏分支预测。这为每种类型专门化整个类,因此每个类都可以独立地预测分支。 - Stephen Lin
(它基本上允许几乎零开销的 Pimpl,在限制内。) - Stephen Lin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接