首页 文章

哪个更快:堆栈分配或堆分配

提问于
浏览
463

这个问题可能听起来相当简单,但这是我与另一位与我合作的开发人员的辩论 .

我正在小心处理堆栈分配的东西,而不是堆分配它们 . 他正在跟我说话,看着我的肩膀并评论说没有必要,因为他们的表现是明智的 .

我总是认为增加堆栈是恒定的时间,并且堆分配的性能取决于堆的当前复杂性(用于找到适当大小的孔)和解除分配(折叠孔以减少碎片,如如果我没有弄错的话,许多标准库实现在删除过程中需要时间来执行此操作 .

这让我觉得可能非常依赖于编译器 . 特别是对于这个项目,我使用Metrowerks编译器来实现PPC架构 . 对这种组合的洞察力将是最有帮助的,但总的来说,对于GCC和MSVC,情况如何?堆分配不如堆栈分配高吗?没有区别吗?或者差异如此微小,它变得毫无意义的微优化 .

23 回答

  • 2

    您可以为特定大小的对象编写特殊的堆分配器,这些对象非常高效 . 但是,通用堆分配器不是特别高效 .

    我同意TorbjörnGyllebring关于物体的预期寿命 . 好点子!

  • 111

    堆栈分配要快得多,因为它真正做的就是移动堆栈指针 . 使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的麻烦 .

    此外,堆栈与堆不仅是性能考虑因素;它还告诉你很多关于对象的预期寿命 .

  • 17

    堆栈速度更快 . 它实际上只在大多数架构上使用单个指令,在大多数情况下,例如,在x86上:

    sub esp, 0x10
    

    (这会将堆栈指针向下移动0x10字节,从而“分配”这些字节以供变量使用 . )

    当然,堆栈的大小非常非常有限,因为您将很快发现是否过度使用堆栈分配或尝试进行递归:-)

    此外,没有理由优化不可验证地需要它的代码的性能,例如通过分析证明 . “过早优化”经常会导致更多问题而不是它的 Value .

    我的经验法则:如果我知道我在编译时需要一些数据,而且它的大小只有几百个字节,我就会进行堆栈分配 . 否则我堆分配它 .

  • 6

    老实说,编写一个比较性能的程序是微不足道的:

    #include <ctime>
    #include <iostream>
    
    namespace {
        class empty { }; // even empty classes take up 1 byte of space, minimum
    }
    
    int main()
    {
        std::clock_t start = std::clock();
        for (int i = 0; i < 100000; ++i)
            empty e;
        std::clock_t duration = std::clock() - start;
        std::cout << "stack allocation took " << duration << " clock ticks\n";
        start = std::clock();
        for (int i = 0; i < 100000; ++i) {
            empty* e = new empty;
            delete e;
        };
        duration = std::clock() - start;
        std::cout << "heap allocation took " << duration << " clock ticks\n";
    }
    

    据说a foolish consistency is the hobgoblin of little minds . 显然优化编译器是许多程序员的大块脚本' minds. This discussion used to be at the bottom of the answer, but people apparently can'很难读到那么远,所以我已经回答了问题 .

    An optimizing compiler may notice that this code does nothing, and may optimize it all away. It is the optimizer's job to do stuff like that, and fighting the optimizer is a fool's errand.

    I would recommend compiling this code with optimization turned off because there is no good way to fool every optimizer currently in use or that will be in use in the future.

    Anybody who turns the optimizer on and then complains about fighting it should be subject to public ridicule.

    如果我关心纳秒精度,我就不会使用 std::clock() . 如果我想将结果作为博士论文发表,我会就此做出更大的贡献,我可能会比较GCC,Tendra / Ten15,LLVM,Watcom,Borland,Visual C,Digital Mars,ICC和其他编译器 . 实际上,堆分配需要比堆栈分配长几百倍,而且我没有看到任何进一步调查问题的任何有用信息 .

    优化器的任务是摆脱我正在测试的代码 . 我没有看到任何理由告诉优化器运行然后尝试欺骗优化器而不是实际优化 . 但是,如果我看到这样做的 Value ,我会做以下一个或多个:

    • 将数据成员添加到 empty ,并在循环中访问该数据成员;但是,如果我只读取数据成员,优化器可以进行常量折叠并删除循环;如果我只写入数据成员,优化器可能会跳过除循环的最后一次迭代之外的所有迭代 . 另外,问题不是"stack allocation and data access vs. heap allocation and data access."

    • 声明 e volatilebut volatile is often compiled incorrectly(PDF) .

    • 在循环中获取 e 的地址(并且可以将其分配给声明为 extern 并在另一个文件中定义的变量) . 但即使在这种情况下,编译器可能会注意到 - 至少在堆栈上 - e 将始终在同一个内存地址分配,然后像上面(1)中那样进行常量折叠 . 我得到了循环的所有迭代,但实际上从未分配过对象 .

    除了显而易见的,这个测试是有缺陷的,因为它测量分配和释放,原始问题没有询问释放 . 当然,在堆栈上分配的变量会在其作用域的末尾自动解除分配,因此不会调用 delete 会(1)使数字倾斜(堆栈释放包含在堆栈分配的数字中,因此测量堆释放是公平的) (2)导致内存泄漏非常糟糕,除非我们保留对新指针的引用并在我们进行时间测量后调用 delete .

    在我的机器上,在Windows上使用g 3.4.4,对于任何小于100000的分配,我得到堆栈和堆分配的“0时钟滴答”,即使这样,我得到堆栈分配的“0时钟滴答”和“15个时钟滴答” “用于堆分配 . 当我测量10,000,000分配,堆栈分配需要31个时钟周期,堆分配需要1562个时钟周期 .


    是的,优化编译器可能会忽略创建空对象 . 如果我理解正确,它甚至可能会忽略整个第一个循环 . 当我将迭代次数提高到10,000,000时,堆栈分配需要31个时钟周期,堆分配需要1562个时钟周期 . 我认为可以安全地说,在没有告诉g优化可执行文件的情况下,g并没有忽略构造函数 .


    自从我写这篇文章以来的几年里,Stack Overflow的偏好就是从优化版本中发布性能 . 总的来说,我认为这是正确的 . 但是,我仍然认为,当您实际上不希望优化代码时,要求编译器优化代码是愚蠢的 . 这让我觉得非常类似于为代客泊车支付额外费用,但拒绝交出钥匙 . 在这种特殊情况下,我不希望优化器运行 .

    使用略微修改的基准测试版本(以解决原始程序每次在循环中没有在堆栈上分配某些内容的有效点)并进行编译而不进行优化但链接到发布库(以解决我们不知道的有效点)我想包括链接到调试库导致的任何减速:

    #include <cstdio>
    #include <chrono>
    
    namespace {
        void on_stack()
        {
            int i;
        }
    
        void on_heap()
        {
            int* i = new int;
            delete i;
        }
    }
    
    int main()
    {
        auto begin = std::chrono::system_clock::now();
        for (int i = 0; i < 1000000000; ++i)
            on_stack();
        auto end = std::chrono::system_clock::now();
    
        std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    
        begin = std::chrono::system_clock::now();
        for (int i = 0; i < 1000000000; ++i)
            on_heap();
        end = std::chrono::system_clock::now();
    
        std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
        return 0;
    }
    

    显示:

    on_stack took 2.070003 seconds
    on_heap took 57.980081 seconds
    

    在使用命令行 cl foo.cc /Od /MT /EHsc 编译时在我的系统上 .

    您可能不同意我获取非优化构建的方法 . 没关系:随意修改基准测试 . 当我打开优化时,我得到:

    on_stack took 0.000000 seconds
    on_heap took 51.608723 seconds
    

    不是因为堆栈分配实际上是瞬时的,而是因为任何半合适的编译器都会注意到 on_stack 没有做任何有用的事情并且可以进行优化 . 我的Linux笔记本电脑上的GCC也注意到 on_heap 没有做任何有用的事情,并且也将其优化掉:

    on_stack took 0.000003 seconds
    on_heap took 0.000002 seconds
    
  • 3

    我在Xbox 360 Xenon处理器上学到的关于堆栈与堆分配的一个有趣的事情,也可能适用于其他多核系统,是在堆上分配导致关键部分被输入以停止所有其他核心,以便alloc不会没有冲突 . 因此,在紧密循环中,堆栈分配是固定大小的阵列的方法,因为它可以防止停顿 .

    如果您正在编写多核/多进程代码,这可能是另一个要考虑的加速,因为您的堆栈分配只能由运行您的作用域函数的核心查看,并且不会影响任何其他核心/ CPU .

  • 3

    我不认为堆栈分配和堆分配通常是可互换的 . 我也希望它们的性能足以满足一般用途 .

    我强烈推荐小件物品,无论哪一件更适合分配范围 . 对于大型项目,堆可能是必要的 .

    在具有多个线程的32位操作系统上,堆栈通常相当有限(尽管通常至少为几mb),因为地址空间需要被分割,迟早一个线程堆栈将运行到另一个线程堆栈中 . 在单线程系统(无论如何都是Linux glibc单线程)上,限制要少得多,因为堆栈可以增长和增长 .

    在64位操作系统上,有足够的地址空间使线程堆栈非常大 .

  • 6

    通常堆栈分配只是从堆栈指针寄存器中减去 . 这比搜索堆快得多 .

    有时堆栈分配需要添加一个或多个虚拟内存页面 . 添加新的归零内存页面不需要从磁盘读取页面,因此通常这比搜索堆快得多(特别是如果堆的一部分也被分页) . 在极少数情况下,你可以构造这样一个例子,在RAM的一部分堆中已经有足够的空间可用,但是为堆栈分配一个新页面必须等待其他一些页面被写出来到磁盘 . 在这种罕见的情况下,堆更快 .

  • 3

    除了堆分配的数量级性能优势之外,堆栈分配对于长时间运行的服务器应用程序更为可取 . 即使是最好的托管堆也最终变得如此分散,以至于应用程序性能下降 .

  • 1

    堆栈的容量有限,而堆却不是 . 进程或线程的典型堆栈大约为8K . 分配后,您无法更改大小 .

    堆栈变量遵循范围规则,而堆栈变量不遵循范围规则 . 如果指令指针超出了函数,则与该函数关联的所有新变量都将消失 .

    最重要的是,您无法提前预测整个函数调用链 . 因此,只需200字节的分配可能会导致堆栈溢出 . 如果您正在编写库而不是应用程序,这一点尤为重要 .

  • 3

    我认为生命是至关重要的,分配的东西是否必须以复杂的方式构建 . 例如,在事务驱动的建模中,您通常必须填充并将带有一堆字段的事务结构传递给操作函数 . 以OSCI SystemC TLM-2.0标准为例 .

    在靠近操作调用的堆栈上分配这些往往会导致巨大的开销,因为构造很昂贵 . 好的方法是在堆上分配并通过池化或简单的策略重用事务对象,例如“此模块只需要一个事务对象” .

    这比在每个操作调用上分配对象快许多倍 .

    原因很简单,该物体具有昂贵的结构和相当长的使用寿命 .

    我会说:尝试两种方法,看看哪种情况最适合你的情况,因为它实际上取决于代码的行为 .

  • 6

    堆分配与堆栈分配的最大问题可能是一般情况下的堆分配是无限制的操作,因此在时间成为问题时你不能使用它 .

    对于定时不是问题的其他应用程序,它可能无关紧要,但如果堆分配很多,这将影响执行速度 . 总是尝试将堆栈用于短期和经常分配的内存(例如在循环中),并尽可能长时间 - 在应用程序启动期间进行堆分配 .

  • 2

    这不是jsut堆栈分配更快 . 你在使用堆栈变量方面也获得了很多 . 他们有更好的参考地点 . 最后,解除分配也便宜得多 .

  • -1

    堆栈分配几乎总是比堆分配快或快,尽管堆分配器当然可以简单地使用基于堆栈的分配技术 .

    但是,在处理堆栈与基于堆的分配的整体性能时(或稍微更好的术语,本地与外部分配),存在更大的问题 . 通常,堆(外部)分配很慢,因为它处理许多不同类型的分配和分配模式 . 减少正在使用的分配器的范围(使其成为算法/代码的本地)将倾向于在不进行任何重大更改的情况下提高性能 . 为分配模式添加更好的结构,例如,在分配和释放对上强制执行LIFO排序,也可以通过以更简单,更结构化的方式使用分配器来提高分配器的性能 . 或者,您可以使用或编写针对您的特定分配模式调整的分配器;大多数程序经常分配一些离散大小,因此基于几个固定(最好是已知)大小的后备缓冲区的堆将表现得非常好 . 出于这个原因,Windows使用其低碎片堆 .

    另一方面,如果你有太多的线程,在32位内存范围上基于堆栈的分配也充满了危险 . 堆栈需要一个连续的内存范围,因此您拥有的线程越多,它们在没有堆栈溢出的情况下运行所需的虚拟地址空间就越多 . 这对于64位来说不会是一个问题(现在),但它肯定会在长时间运行的程序中造成严重破坏 . 由于碎片而耗尽虚拟地址空间总是很难处理 .

  • 4

    堆栈分配是一对指令,而我所知的最快的rtos堆分配器(TLSF)平均使用150个指令 . 堆栈分配也不需要锁定,因为它们使用线程本地存储,这是另一个巨大的性能获胜 . 因此,堆栈分配可以快2-3个数量级,具体取决于您的环境多线程程度 .

    通常,如果您关心性能,堆分配是您最后的选择 . 一个可行的中间选项可以是一个固定的池分配器,它也只是一对指令,并且每个分配开销很少,因此对于小的固定大小的对象来说非常好 . 在缺点方面它只适用于固定大小的对象,本质上不是线程安全的并且具有块碎片问题 .

  • 1

    关于这种优化有一个普遍的观点 .

    The optimization you get is proportional to the amount of time the program counter is actually in that code.

    如果您对程序计数器进行采样,您将找到它花费时间的位置,这通常只是代码的一小部分,并且通常在库例程中您无法控制 .

    只有当你发现在对象的堆分配中花费了很多时间时,堆栈分配它们才会明显加快 .

  • 454

    正如其他人所说,堆栈分配通常要快得多 .

    但是,如果您的对象复制成本很高,那么在您使用这些对象时,如果您不小心,在堆栈上进行分配可能会导致巨大的性能损失 .

    例如,如果您在堆栈上分配了一些内容,然后将其放入容器中,那么分配就更好了在堆上并将指针存储在容器中(例如,使用std :: shared_ptr <>) . 如果按值传递或返回对象,以及其他类似的方案,情况也是如此 .

    关键是虽然在很多情况下堆栈分配通常比堆分配更好,但有时如果你在最不适合计算模型的情况下不想使用堆栈分配,它可能会导致比它解决的问题更多的问题 .

  • 155
    class Foo {
    public:
        Foo(int a) {
    
        }
    }
    int func() {
        int a1, a2;
        std::cin >> a1;
        std::cin >> a2;
    
        Foo f1(a1);
        __asm push a1;
        __asm lea ecx, [this];
        __asm call Foo::Foo(int);
    
        Foo* f2 = new Foo(a2);
        __asm push sizeof(Foo);
        __asm call operator new;//there's a lot instruction here(depends on system)
        __asm push a2;
        __asm call Foo::Foo(int);
    
        delete f2;
    }
    

    在asm中会是这样的 . 当你在 func 时, f1 和指针 f2 已经在堆栈上分配(自动存储) . 顺便说一句,Foo f1(a1) 对堆栈指针没有指令效果( esp ),它已被分配,如果 func 想要获得成员 f1 ,它的指令是这样的: lea ecx [ebp+f1], call Foo::SomeFunc() . 堆栈分配的另一件事可能会让某人认为内存类似于 FIFOFIFO 只是在你进入某个函数时发生的,如果你在函数中并分配类似 int i = 0 的东西,那么就没有发生过推送 .

  • 27

    之前已经提到堆栈分配只是移动堆栈指针,即大多数架构上的单个指令 . 将其与堆分配时通常发生的情况进行比较 .

    操作系统将部分空闲存储器维护为链表,其中有效载荷数据包括指向空闲部分的起始地址和空闲部分的大小的指针 . 为了分配X字节的内存,遍历链接列表并按顺序访问每个音符,检查其大小是否至少为X.当找到大小为P> = X的部分时,P被分成两部分,尺寸X和PX . 更新链接列表并返回指向第一部分的指针 .

    正如您所看到的,堆分配取决于可能的因素,例如您请求的内存量,内存碎片的数量等等 .

  • 0

    通常,堆栈分配比堆分配更快,如上面的几乎每个答案所提到的 . 堆栈推送或弹出是O(1),而从堆中分配或释放可能需要先行分配 . 但是,通常不应该在紧密的,性能密集型循环中进行分配,因此选择通常会归结为其他因素 .

    做出这种区分可能会很好:你可以在堆上使用“堆栈分配器” . 严格来说,我认为堆栈分配意味着实际的分配方法而不是分配的位置 . 如果你在实际的程序堆栈上分配了很多东西,那可能因为各种原因而变坏 . 另一方面,在可能的情况下使用堆栈方法在堆上进行分配是您可以为分配方法做出的最佳选择 .

    既然你提到了Metrowerks和PPC,我猜你的意思是Wii . 在这种情况下,内存非常宝贵,并且尽可能使用堆栈分配方法保证您不会在内存上浪费内存 . 当然,这样做需要比“普通”堆分配方法更多的关注 . 评估每种情况的权衡是明智的 .

  • 3

    注意在选择堆栈与堆分配时,注意事项通常与速度和性能无关 . 堆栈就像一个堆栈,这意味着它非常适合推送块并再次弹出它们,最后输出 . 程序的执行也是堆栈式的,最后输入的程序首先退出 . 在大多数编程语言中,过程中所需的所有变量仅在过程执行期间可见,因此在进入过程时会被推送,并在退出或返回时弹出堆栈 .

    现在举例说明无法使用堆栈:

    Proc P
    {
      pointer x;
      Proc S
      {
        pointer y;
        y = allocate_some_data();
        x = y;
      }
    }
    

    如果在过程S中分配一些内存并将其放在堆栈上然后退出S,则分配的数据将从堆栈中弹出 . 但是P中的变量x也指向该数据,因此x现在指向堆栈指针下面的某个位置(假设堆栈向下增长)具有未知内容 . 如果堆栈指针刚刚向上移动而不清除其下面的数据,则内容可能仍然存在,但是如果您开始在堆栈上分配新数据,则指针x实际上可能指向该新数据 .

  • 2

    特定于C语言的问题

    首先, there is no so-called "stack" or "heap" allocation mandated by C++ . 如果您在讨论块范围内的自动对象,它们甚至不是"allocated" . (顺便说一句,C中的自动存储持续时间肯定与"allocated"不同;后者在C语言中是"dynamic" . )动态分配的内存在免费存储上,不一定在"the heap"上,尽管后者通常是(默认的) )实施 .

    虽然根据抽象语义规则,自动对象仍然占用内存,但是当它可以证明这无关紧要时(当它不改变程序的可观察行为时),允许符合C的实现忽略这一事实 . 此权限由 . 授予ISO C中的as-if规则,它也是启用常规优化的通用子句(并且在ISO C中也存在几乎相同的规则) . 除了as-if规则,ISO C还具有复制省略规则,以允许省略对象的特定创建 . 由此省略了所涉及的构造函数和析构函数调用 . 因此,与源代码隐含的朴素抽象语义相比,这些构造函数和析构函数中的自动对象(如果有)也被消除了 .

    另一方面,免费商店分配肯定是"allocation"设计 . 在ISO C规则下,可以通过调用分配函数来实现这种分配 . 但是,由于ISO C 14,在特定情况下允许合并全局分配函数(即 ::operator new )调用的新(非假设)规则 . 因此,动态分配操作的一部分也可以像自动对象一样无操作 .

    分配功能分配内存资源 . 可以使用分配器基于分配进一步分配对象 . 对于自动对象,它们是直接呈现的 - 虽然可以访问底层内存并用于为其他对象提供内存(通过放置 new ),但这并不像免费商店那样有意义,因为无法移动其他地方的资源 .

    所有其他问题都超出了C的范围 . 尽管如此,它们仍然很重要 .

    关于C的实现

    C不公开已知的激活记录或某些一流的延续(例如由着名的 call/cc ),没有办法直接操纵激活记录框架 - 实现需要将自动对象放置在其中 . 一旦没有(非可移植的)与底层实现的互操作("native"非可移植代码,例如内联汇编代码),遗漏帧的底层分配可能非常简单 . 例如,当内联被调用函数时,帧可以有效地合并到其他帧中,因此无法显示"allocation"是什么 .

    但是,一旦互操作得到尊重,事情就变得越来越复杂 . C的典型实现将公开ISA(指令集架构)上的互操作的能力,其中一些调用约定作为与本机(ISA级机器)代码共享的二进制边界 . 特别是在维护堆栈指针时,这显然是昂贵的,堆栈指针通常由ISA级寄存器直接保存(可能有特定的机器指令访问) . 堆栈指针指示(当前活动的)函数调用的顶部框架的边界 . 当进入函数调用时,需要一个新的帧,并且堆栈指针被添加或减去(取决于ISA的约定)一个不小于所需帧大小的值 . 然后,当堆栈指针操作后,表示 allocated . 根据用于调用的调用约定,函数的参数也可以传递到堆栈帧 . 该帧可以保存由C源代码指定的自动对象(可能包括参数)的存储器 . 在这种实现的意义上,这些对象是"allocated" . 当控件退出函数调用时,不再需要该帧,通常通过将堆栈指针恢复回调用之前的状态(先前根据调用约定保存)来释放它 . 这可以被视为"deallocation" . 这些操作使激活记录有效地成为LIFO数据结构,因此通常称为“the (call) stack” . 堆栈指针有效地指示堆栈的顶部位置 .

    因为大多数C实现(特别是针对ISA级本机代码并使用汇编语言作为其直接输出的实现)使用类似这样的策略,所以这种令人困惑的"allocation"方案很受欢迎 . 这样的分配(以及解除分配)确实花费了机器周期,并且当(非优化的)调用频繁发生时它可能很昂贵,即使现代CPU微体系结构可以通过硬件为公共代码模式实现复杂的优化(例如使用堆栈引擎实现 PUSH / POP 指令) .

    但无论如何,一般来说, it is true that the cost of stack frame allocation is significantly less than a call to an allocation function operating the free store (unless it is totally optimized away) ,它本身可以拥有数百个(如果不是数百万个:-)操作来维护堆栈指针和其他状态 . 分配功能通常基于托管环境提供的API(例如,由OS提供的运行时) . 与为函数调用保存自动对象的目的不同,这种分配是通用的,因此它们不具有像堆栈那样的帧结构 . 传统上,它们从称为heap(或几个堆)的池存储中分配空间 . 与"stack"不同,这里的概念"heap"并不表示正在使用的数据结构; it is derived from early language implementations decades ago . (顺便说一句,调用堆栈通常由程序中的环境从堆中分配固定或用户指定的大小或线程启动 . )用例的本质使得从堆中分配和解除分配变得更加复杂(比推送或弹出堆栈帧),并且几乎不可能通过硬件直接优化 .

    对内存访问的影响

    通常的堆栈分配总是将新帧放在顶部,因此它具有非常好的局部性 . 这对缓存很友好 . OTOH,在免费商店中随机分配的内存没有这样的属性 . 从ISO C 17开始, <memory> 提供了池资源模板 . 这种接口的直接目的是允许连续分配的结果在存储器中靠近在一起 . 这承认了这一策略通常有利于当代实施的性能,例如,在现代建筑中保持友好 . 但这是关于访问而不是分配的性能 .

    并发

    期望并发访问内存可能会在堆栈和堆之间产生不同的影响 . 调用堆栈通常由C实现中的一个执行线程专有 . OTOH,堆通常在进程中的线程之间共享 . 对于这样的堆,分配和释放功能必须保护共享的内部管理数据结构免受数据竞争的影响 . 因此,由于内部同步操作,堆分配和解除分配可能会产生额外的开销 .

    空间效率

    由于用例和内部数据结构的性质,堆可能会受到内部memory fragmentation的影响,而堆栈却没有 . 这对内存分配的性能没有直接影响,但在具有virtual memory的系统中,低空间效率可能会降低内存访问的整体性能 . 当HDD用作物理内存的交换时,这尤其可怕 . 它可能导致相当长的延迟 - 有时数十亿次 .

    堆栈分配的限制

    尽管堆栈分配在性能上通常优于堆分配,但实际上并不意味着堆栈分配总是可以取代堆分配 .

    首先,无法使用ISO C以便携方式在运行时指定的堆栈上分配空间 . 像 alloca 和G的VLA(可变长度数组)这样的实现提供了扩展,但有理由避免使用它们 . (IIRC,Linux源最近删除了对VLA的使用 . )(另请注意,ISO C99确实有VLA,但ISO C11将支持变为可选 . )

    其次,没有可靠和便携的方法来检测堆栈空间耗尽 . 这通常被称为堆栈溢出(嗯,这个站点的词源),但可能更累积,"stack overrun" . 实际上,这经常导致无效的内存访问,然后程序的状态被破坏(......或者更糟糕的是,安全漏洞) . 事实上,ISO C没有堆栈概念和makes it undefined behavior when the resource is exhausted . 请注意自动物体应留出多少空间 .

    如果堆栈空间用完,堆栈中分配的对象太多,这可能是由于函数的活动调用太多或自动对象使用不当造成的 . 这种情况可能表明存在错误,例如没有正确退出条件的递归函数调用 .

    然而,有时需要深度递归调用 . 在需要支持未绑定活动调用的语言的实现中(调用深度仅受总内存限制),不可能直接使用本机调用栈作为目标语言激活记录,如典型的C实现 . 例如,SML/NJ显式地在堆上分配帧并使用cactus stacks . 这种激活记录帧的复杂分配通常不像调用栈帧那样快 . 但是,当使用proper tail recursion进一步实现语言时,在对象语言中直接进行堆栈分配(即,语言中的"object"不作为引用存储,而是可以一对一映射到非共享C对象的原始值)更多一般而言,性能损失更多 . 使用C实现此类语言时,很难估计性能影响 .

  • 1

    永远不要过早假设,因为其他应用程序代码和使用会影响您的功能 . 所以看功能是隔离是没有用的 .

    如果您对应用程序认真,那么请对其进行VTune或使用任何类似的分析工具并查看热点 .

    科坦

  • 1

    我想说实际代码由GCC生成(我还记得VS) doesn't have overhead to do stack allocation .

    说以下功能:

    int f(int i)
      {
          if (i > 0)
          {   
              int array[1000];
          }   
      }
    

    以下是代码生成:

    __Z1fi:
      Leh_func_begin1:
          pushq   %rbp
      Ltmp0:
          movq    %rsp, %rbp
      Ltmp1:
          subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
      Ltmp2:
          movl    %edi, -4(%rbp)
          movl    -8(%rbp), %eax
          addq    $3880, %rsp
          popq    %rbp
          ret 
      Leh_func_end1:
    

    因此,如果您拥有多少局部变量(即使在内部或切换),只需3880将变为另一个值 . 除非你没有局部变量,否则只需要执行该指令 . 所以分配局部变量没有开销 .

相关问题