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

问题描述 投票:8回答:4

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

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

  • 2-带有代码的每个代码块的分区析构函数,并且仅通过需要的内容进行“意大利面条跳转”。有利的一面是-没有重复的析构函数,不利的一面-它涉及到非顺序执行和跳转,还涉及额外的隐藏变量和条件,例如,确定执行是否离开某个块以继续在父级执行将是必需的阻止或中断/继续/转到/返回,这也增加了其复杂性。而且,额外的变量和检查很可能会消耗掉此方法所节省的空间,具体取决于对象的数量以及其中的结构和控制流的复杂性。

而且我知道通常对此类问题的回答是“同时做,分析和决定”,如果这是一项琐碎的任务,那就是我会做的,但是事实证明编写全功能的编译器有些艰巨,所以我更愿意找一些专家输入而不是建两个桥,看看哪个桥更好,然后烧另一个桥。

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

c++ oop compiler-construction destructor control-flow
4个回答
4
投票

在大多数情况下,析构函数调用可以与普通函数调用相同的方式对待。

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

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

如果在不久的将来似乎可以进行优化,则将上述调用与其他任何方法一样对待:它是否可以与其他所有内容一起放入缓存?这样做会占用图像中过多的空间吗?等等。

前端可以在其输出AST中每个可操作块的末尾插入对非平凡析构函数的“调用”。

后端可能将诸如普通函数调用之类的东西,将它们连接在一起,在某个地方进行大的块O析构函数调用逻辑,然后跳转到该处,等等...

将函数链接到相同的逻辑似乎很常见。例如,MSC倾向于将所有琐碎的功能链接到相同的实现,析构函数或其他方法,无论是否进行优化。

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

另外一件事:

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


2
投票

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

内联呼叫是一种优化,除非您“知道它会有所收获”(代码大小与速度),否则您不应该这样做。

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

[1]商业编译器有一个特殊的表,基于“抛出异常的位置”,并生成了一段代码来进行清理-通常通过在每个清理块中都具有多个跳转标签,对许多异常点重复使用同一清理。


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代码上,通过表处理异常,这些表将异常发生时的指令指针映射到相应的代码清除块。


1
投票

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

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

对于GCC(它是一个自由软件编译器-Clang/LLVM也是如此,因此您可以研究其源代码),您可能可以尝试使用-fdump-tree-all编译一些简单的C ++测试用例代码,然后查看它是在gimplification时间完成的。顺便说一句,您可以使用g++自定义MELT以探索其内部表示形式。

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

© www.soinside.com 2019 - 2024. All rights reserved.