C ++:类特化是一个符合标准的编译器的有效转换?

时间:2020-12-10 13:57:37

Hopefully this isn't too specialized of a question for *: if it is and could be migrated elsewhere let me know...

希望这对于*的问题不太专业:如果它是并且可以迁移到别处让我知道......

Many moons ago, I wrote a undergraduate thesis proposing various devirtualization techniques for C++ and related languages, generally based on the idea of precompiled specialization of code paths (somewhat like templates) but with checks to choose the correct specializations are chosen at runtime in cases where they cannot be selected at compile-time (as templates must be).

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

The (very) basic idea is something like the following...suppose you have a class C like the following:

(非常)基本的想法类似于以下内容...假设您有类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
};

And you knew that there exist concrete subclasses of Foo like FooA, FooB, etc, with known complete types (without necessarily having an exhaustive list), then you could precompile specialized versions of C for some selected subclasses of Foo, like, for example (note the constructor is not included here, purposely, since it won't be called):

而且你知道存在Foo的具体子类,如FooA,FooB等,具有已知的完整类型(不一定有详尽的列表),那么你可以为一些选定的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;
};

And replace the constructor of C with something like the following:

并用以下内容替换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
}

So basically, the dynamic type of the object being constructed is changed based on the dynamic type of the arguments to its constructor. (Note, you can't do this with templates because you can only create a C<Foo> if you know the type of f at compile-time). From now on, any call to FooA::bark() through C::quack() only involves one virtual call: either the call to C::quack() is statically bound to the non-specialized version which dynamically calls FooA::bark(), or the call to C::quack() is dynamically forwarded to C_FooA::quack() which statically calls FooA::bark(). Furthermore, dynamic dispatch might be eliminated completely in some cases if the flow analyzer has enough information to make a static call to C_FooA::quack(), which could be very useful in a tight loop if it allows inlining. (Although technically at that point you'd probably be OK even without this optimization...)

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

(Note that this transformation is safe, although less useful, even if _f is non-const and protected instead of private and C is inherited from a different translation unit...the translation unit creating the vtable for the inherited class won't know anything at all about the specializations and the constructor of the inherited class will just set the this->vptr to its own vtable, which will not reference any specialized functions because it won't know anything about them.)

(请注意,这种转换是安全的,虽然不太有用,即使_f是非const而且受保护而不是私有,而C是从不同的翻译单元继承的...创建继承类的vtable的翻译单元将不知道关于继承类的特化和构造函数的任何事情都只是将this-> vptr设置为它自己的vtable,它不会引用任何专门的函数,因为它不会对它们有所了解。)

This might seem like a lot of effort to eliminate one level of indirection, but the point is that you can do it to any arbitrary nesting level (any depth of virtual calls following this pattern could be reduced to one) based only on local information within a translation unit, and do it in a way that's resilient even if new types are defined in other translation units that you don't know about...you just might add a lot of code bloat that you wouldn't have otherwise if you did it naively.

这似乎需要付出很多努力才能消除一个级别的间接,但关键是你可以根据内部的本地信息将它做到任意嵌套级别(此模式后的任何深度的虚拟调用可以减少到一个)一个翻译单元,即使在你不了解的其他翻译单元中定义了新类型,也能以一种有弹性的方式进行...你可能会添加很多代码膨胀,否则如果你没有天真地做了。

Anyway, independent of whether this kind of optimization would really have enough bang-for-the-buck be worth the effort of implementation and also worth the space overhead in the resulting executable, my question is, is there anything in Standard C++ which would prevent a compiler from performing such a transformation?

无论如何,独立于这种优化是否真的具有足够的爆炸效果值得实现的努力,并且还值得产生的可执行文件中的空间开销,我的问题是,标准C ++中有什么可以阻止编译器执行这样的转换?

My feeling is no, since the standard doesn't specify at all how virtual dispatch is done or how pointers-to-member-functions are represented. I'm pretty sure there's nothing about the RTTI mechanism preventing C and C_FooA from masquerading as the same type for all purposes, even if they have different virtual tables. The only other thing I could think of that could possibly matter is some close reading of the ODR, but probably not.

我的感觉是否定的,因为标准根本没有说明虚拟调度是如何完成的,或者指示如何表示指向成员函数的指针。我非常确定RTTI机制没有任何关于防止C和C_FooA伪装成所有目的的相同类型,即使它们具有不同的虚拟表。我能想到的唯一另一件可能很重要的事情就是仔细阅读ODR,但可能并非如此。

Am I overlooking something? Barring ABI/linking issues, would transformations like this be possible without breaking conforming C++ programs? (Furthermore, if yes, could this be done currently with the Itanium and/or MSVC ABIs? I'm fairly sure the answer there is yes, as well, but hopefully someone can confirm.)

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

EDIT: Does anyone know if anything like this is implemented in any mainstream compiler/JIT for C++, Java, or C#? (See discussion and linked chat in the comments below...) I'm aware JITs do speculative static-binding/inlining of virtuals directly at call sites, but I don't know if they do anything like this (with entirely new vtables being generated and chosen based on a single type check done at the constructor, rather than at each call site).

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

2 个解决方案

#1


1  

Is there anything in Standard C++ which would prevent a compiler from performing such a transformation?

标准C ++中是否存在阻止编译器执行此类转换的任何内容?

Not if you're sure the observable behavior is unchanged - that's the "as-if rule" which is Standard section 1.9.

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

But this might make proving that your transformation is correct pretty difficult: 12.7/4:

但这可能会证明你的转变是非常困难的:12.7 / 4:

When a virtual function is called directly or indirectly from a constructor (including the mem-initializer or brace-or-equal-initializer for a non-static data member) or from a destructor, and the object to which the call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor's own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor's own class, or overriding it in one of the other base classes of the most derived object.

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

So if the destructor Foo::~Foo() happens to directly or indirectly call C::quack() on an object c, where c._f points at the object being destroyed, you need to call Foo::bark(), even if _f was a FooA when you constructed object c.

因此,如果析构函数Foo ::〜Foo()恰好在对象c上直接或间接调用C :: quack(),其中c._f指向被销毁的对象,则需要调用Foo :: bark(),即使构造对象c时_f是FooA。

#2


0  

On first reading, this sounds like a c++-focused variation of polymorphic inline caching. I think that V8 and Oracle's JVM both use it, and I know that .NET does.

在第一次阅读时,这听起来像是一个以c ++为中心的多态内联缓存变体。我认为V8和Oracle的JVM都使用它,我知道.NET确实如此。

To answer your original question: I don't think there's anything in the standard that forbids these kinds of implementations. C++ takes the "as-is" rule quite seriously; so long as you faithfully implement the right semantics, you can do the implementation in any crazy way you like. c++ virtual calls aren't very complicated, so I doubt you'd trip over any edge cases there either (unlike if, say, you were trying to do something clever with static binding).

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

#1


1  

Is there anything in Standard C++ which would prevent a compiler from performing such a transformation?

标准C ++中是否存在阻止编译器执行此类转换的任何内容?

Not if you're sure the observable behavior is unchanged - that's the "as-if rule" which is Standard section 1.9.

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

But this might make proving that your transformation is correct pretty difficult: 12.7/4:

但这可能会证明你的转变是非常困难的:12.7 / 4:

When a virtual function is called directly or indirectly from a constructor (including the mem-initializer or brace-or-equal-initializer for a non-static data member) or from a destructor, and the object to which the call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor's own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor's own class, or overriding it in one of the other base classes of the most derived object.

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

So if the destructor Foo::~Foo() happens to directly or indirectly call C::quack() on an object c, where c._f points at the object being destroyed, you need to call Foo::bark(), even if _f was a FooA when you constructed object c.

因此,如果析构函数Foo ::〜Foo()恰好在对象c上直接或间接调用C :: quack(),其中c._f指向被销毁的对象,则需要调用Foo :: bark(),即使构造对象c时_f是FooA。

#2


0  

On first reading, this sounds like a c++-focused variation of polymorphic inline caching. I think that V8 and Oracle's JVM both use it, and I know that .NET does.

在第一次阅读时,这听起来像是一个以c ++为中心的多态内联缓存变体。我认为V8和Oracle的JVM都使用它,我知道.NET确实如此。

To answer your original question: I don't think there's anything in the standard that forbids these kinds of implementations. C++ takes the "as-is" rule quite seriously; so long as you faithfully implement the right semantics, you can do the implementation in any crazy way you like. c++ virtual calls aren't very complicated, so I doubt you'd trip over any edge cases there either (unlike if, say, you were trying to do something clever with static binding).

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