生产编译器如何在流控制上实现析构函数处理

时间:2014-11-02 10:59:11

标签: c++ oop destructor flow-control compiler-construction

长话短说 - 我正在编写一个编译器,并且达到了OOP功能,我面临着涉及处理析构函数的两难问题。基本上我有两个选择:

  • 1 - 将所有析构函数放在程序中该点需要调用的对象上。这个选项听起来性能友好且简单,但会使代码膨胀,因为根据控制流程,某些析构函数可以多次复制。

  • 2 - 每个代码块的分区析构函数,带有标签和" spaghetti jump"只有那些需要的东西。好处 - 没有析构函数将被复制,缺点 - 它将涉及非顺序执行和跳转,以及额外的隐藏变量和条件,这将需要例如确定执行是否离开块以继续在父级执行阻止或中断/继续/转到/返回,这也增加了它的复杂性。额外的变量和检查很可能会占用这种方法所节省的空间,具体取决于对象的数量以及内部复杂的结构和控制流程。

而且我知道对这些问题的通常回应是"做两件事,简介和决定"如果这是一项微不足道的任务我会怎么做,但编写一个功能齐全的编译器已经证明有点艰巨,所以我更喜欢得到一些专家输入,而不是建立两个桥梁,看看哪个更好,烧另一个之一。

我将c ++放在标签中,因为它是我正在使用的语言,并且对它和RAII范例有点熟悉,这也是我的编译器正在建模的。

4 个答案:

答案 0 :(得分:4)

在大多数情况下,析构函数调用的处理方式与普通函数调用相同。

较小的部分是处理EH。我注意到MSC在"普通"中生成了内联析构函数调用的组合。代码,并且,对于x86-64,创建单独的清理代码,其本身可能有也可能没有析构函数逻辑的副本。

IMO,最简单的解决方案是始终将非平凡的析构函数称为普通函数。

如果有可能出现优化,那么就像其他任何事情一样对待上述调用:它是否适合缓存中的所有其他内容?这样做会占用图像中太多的空间吗?等。

前端可以插入"来电"到输出AST中每个可操作块结尾的非平凡析构函数。

后端可以处理普通函数调用之类的东西,将它们连接在一起,在某处创建一个大块o-析构函数调用逻辑并跳转到那个等等......

将功能链接到相同的逻辑似乎很常见。例如,MSC倾向于将所有普通函数链接到相同的实现,析构函数或其他方面,优化与否。

这主要来自经验。像往常一样,YMMV。

还有一件事:

EH清理逻辑往往像跳转表一样工作:对于给定的函数,您可以跳转到单个析构函数调用列表,具体取决于抛出异常的位置(如果适用)。

答案 1 :(得分:2)

我不知道商业编译器如何提出代码,但假设我们忽略了异常[1],我采取的方法是调用析构函数,而不是内联它。每个析构函数都包含该对象的完整析构函数。使用循环来处理数组的析构函数。

内联电话是一种优化,你不应该这样做,除非你知道它是否得到回报" (代码大小与速度)。

你需要在封闭的区块中处理"破坏,但假设你没有跳出障碍,那应该很容易。跳出块(例如返回,中断等)将意味着您必须跳转到一段代码来清理您所在的块。

[1]商业编译器具有基于&#34的特殊表;其中抛出了异常"以及为执行该清理而生成的一段代码 - 通常通过多次跳转为多个异常点重用相同的清理每个清理块中的标签。

答案 2 :(得分:2)

编译器使用两种方法的混合。 MSVC使用内联析构函数调用正常的代码流,并以相反的顺序清理代码块以用于早期返回和异常。在正常流程中,它使用单个隐藏的本地整数来跟踪到目前为止的构造函数进度,因此它知道在早期返回时跳转到何处。单个整数就足够了,因为范围总是形成一个树(而不是说为已经成功构建或未成功构建的每个类使用位掩码)。例如,以下相当短的代码使用了一个带有非平凡析构函数的类,并且在整个过程中散布了一些随机返回...

    ...
    if (randomBool()) return;
    Foo a;
    if (randomBool()) return;
    Foo b;
    if (randomBool()) return;

    {
        Foo c;
        if (randomBool()) return;
    }

    {
        Foo d;
        if (randomBool()) return;
    }
    ...

...可以在x86上扩展为如下所示的伪代码,其中构造函数进度在每次构造函数调用后立即递增(有时多于一个到下一个唯一值)并递减(或者弹出'紧接每个析构函数调用之前的某个值。请注意,具有普通析构函数的类不会影响此值。

    ...
    save previous exception handler // for x86, not 64-bit table based handling
    preallocate stack space for locals
    set new exception handler address to ExceptionCleanup
    set constructor progress = 0
    if randomBool(), goto Cleanup0
    Foo a;
    set constructor progress = 1 // Advance 1
    if randomBool(), goto Cleanup1
    Foo b;
    set constructor progress = 2 // And once more
    if randomBool(), goto Cleanup2

    {
        Foo c;
        set constructor progress = 3
        if randomBool(), goto Cleanup3
        set constructor progress = 2 // Pop to 2 again
        c.~Foo();
    }

    {
        Foo d;
        set constructor progress = 4 // Increment 2 to 4, not 3 again
        if randomBool(), goto Cleanup4
        set constructor progress = 2 // Pop to 2 again
        d.~Foo();
    }

// alternate Cleanup2
    set constructor progress = 1
    b.~Foo();
// alternate Cleanup1
    set constructor progress = 0
    a.~Foo();

Cleanup0:
    restore previous exception handler
    wipe stack space for locals
    return;

ExceptionCleanup:
    switch (constructor progress)
    {
    case 0: goto Cleanup0; // nothing to destroy
    case 1: goto Cleanup1;
    case 2: goto Cleanup2;
    case 3: goto Cleanup3;
    case 4: goto Cleanup4;
    }
    // admitting ignorance here, as I don't know how the exception
    // is propagated upward, and whether the exact same cleanup
    // blocks are shared for both early returns and exceptions.

Cleanup4:
    set constructor progress = 2
    d.~Foo();
    goto Cleanup2;
Cleanup3:
    set constructor progress = 2
    c.~Foo();
    // fall through to Cleanup2;
Cleanup2:
    set constructor progress = 1
    b.~Foo();
Cleanup1:
    set constructor progress = 0
    a.~Foo();
    goto Cleanup0;
    // or it may instead return directly here

编译器当然可以重新排列这些块,无论如何它认为更有效,而不是将所有清理结束。早期的返回可能会跳转到函数末尾的备用Cleanup1 / 2。在64位MSVC代码上,异常通过表来处理,这些表将异常发生时的指令指针映射到相应的代码清理块。

答案 3 :(得分:1)

优化编译器正在转换已编译源代码的内部表示。

它通常构建basic blocks的定向(通常是循环)图。构建此control flow graph时,它会将调用添加到析构函数中。

对于GCC(它是一个免费的软件编译器 - 以及Clang/LLVM - 所以你可以研究它的源代码),你可能会尝试用{编译一些简单的C ++测试用例代码{1}}然后看到它在gimplification时间完成。顺便说一句,您可以使用MELT自定义-fdump-tree-all来探索其内部表示形式。

顺便说一句,我不认为你如何处理析构函数是非常重要的(请注意,在C ++中,它们在语法定义的位置被隐式调用,例如它们的g++定义范围)。这种编译器的大部分工作都在优化(然后,处理析构函数不是很相关;它们几乎就像其他程序一样)。