【问题标题】:Use curly braces to remove allocations from the stack使用花括号从堆栈中删除分配
【发布时间】:2019-07-15 14:47:33
【问题描述】:

假设我在一种方法中有很多堆栈分配。

如果我在一组代码中添加花括号,分配的对象会在超出范围时从堆栈中弹出,还是需要在释放内存之前退出方法?

我还应该补充一点,这是在 MFC 应用程序中完成的。

void LongMethod()
{
    {
        struct someLargeStruct;
        // Do a lot of work and allocations.
    }

    {
        struct anotherLargeStruct;
        // more work here.
    }   
}

【问题讨论】:

  • 没有通用的方法来回答这个问题(谁说目标平台甚至有堆栈?)。查看编译器生成的代码。
  • 它是实现定义的,因为 C++ 标准(和 C 标准)不知道调用堆栈。
  • 虽然堆栈是一种有限的资源,但如果您担心超出该限制,那么解决问题的方法可能不是更多的嵌套或更多的函数。也许问题在更高的层次上(设计、分析甚至需求),而解决方案必须在那个层次上?
  • 作用域(花括号)确保在作用域结束时调用对象析构函数。是否也释放内存是实现定义的,您通常不需要关心。
  • 我对数据包做了很多位移。设计完全取决于我,但用一种或两种方法完成所有事情是有意义的。我想我可以使用堆。到目前为止一切正常。

标签: c++ mfc


【解决方案1】:

只是为了进一步澄清这一点 - 是的,标准要求在块范围的末尾释放自动存储,请参阅[basic.stc.auto]/1

显式声明register 或未显式声明staticextern 的块范围变量具有自动存储持续时间。这些实体的存储将持续到创建它们的块退出。

但是,编译器只需要实现程序的可观察行为 (the as-if rule):

...需要实现来模拟(仅)抽象机器的可观察行为...

由于标准将自动存储视为无限,并且没有其他方法可以观察堆栈使用情况,因此根据 as-if 规则,符合标准的编译器没有义务严格在每个范围的末尾准确释放内存。

我们确实观察到 GCC、clang 和 MSVC 更喜欢在函数启动时分配堆栈空间并在函数退出时取消分配。尽管至少它们似乎在不同的块作用域之间重用了内存:

int do_something_with(int*);
int do_something_with(long long*);

void do_something()
{
    {
        int x[100];
        do_something_with(x);
    }
    {
        long long y[100];
        do_something_with(y);
    }
}

分配的堆栈帧:800 bytesxy 共享相同的空间):

do_something():
        sub     rsp, 808
        mov     rdi, rsp
        call    do_something_with(int*)
        mov     rdi, rsp
        call    do_something_with(long long*)
        add     rsp, 808
        ret

如果没有块作用域,行为会发生轻微变化,我们观察到不再有内存重用:

int do_something_with(int*);
int do_something_with(long long*);

void do_something()
{
    int x[100];
    do_something_with(x);
    long long y[100];
    do_something_with(y);
}

分配的堆栈帧:1200 bytes (x + y):

do_something():
        sub     rsp, 1208
        mov     rdi, rsp
        call    do_something_with(int*)
        lea     rdi, [rsp+400]
        call    do_something_with(long long*)
        add     rsp, 1208
        ret

总而言之,是的,块作用域有一些影响,但不要指望它与这些作用域完全对齐。

在递归函数中尤其令人烦恼 (example):

int do_something_with(long long*);
int bar();

void foo()
{
    {
        if (bar())
            foo(); // don't count on this being efficient
    }
    {
        long long y[10000];
        do_something_with(y);
    }
}

因此,将大量堆栈用户隔离到单独的函数中会更安全。

【讨论】:

    【解决方案2】:

    C++ 中没有stack(至少,不是你想的那样,有std::stack 容器适配器,但完全不同)。

    但是,有一个自动存储,它通常在堆栈上实现,但可能不是(例如,它可能在寄存器中)。

    但是,对于自动存储,

    对象的存储空间在开始时分配 封闭代码块并在最后释放。所有本地对象 有这个存储持续时间,除了那些声明为静态、外部或 thread_local。

    (https://en.cppreference.com/w/cpp/language/storage_duration)

    这意味着如果编译器决定使用堆栈来存储变量,它应该知道它们的存储在封闭块的末尾结束。尽管此时编译器没有义务减少堆栈指针,但如果不这样做,将是一个严重的实现质量问题。

    【讨论】:

    • 对于 C 语言呢?该问题也被标记为 C。
    • 对不起。我删除了 C 标签。
    猜你喜欢
    • 2020-04-08
    • 1970-01-01
    • 2015-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多