ChatGPT解决这个技术问题 Extra ChatGPT

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

这个问题听起来可能相当初级,但这是我与另一位共事的开发人员进行的辩论。

我小心翼翼地在可能的地方堆栈分配东西,而不是堆分配它们。他正在和我说话,看着我的肩膀并评论说这没有必要,因为它们在性能方面是相同的。

我一直认为堆的增长是恒定的时间,堆分配的性能取决于堆的当前复杂性,用于分配(找到适当大小的孔)和取消分配(折叠孔以减少碎片,如如果我没记错的话,许多标准库实现在删除期间需要时间来执行此操作)。

这让我觉得可能非常依赖编译器。特别是对于这个项目,我将 Metrowerks 编译器用于 PPC 架构。了解这种组合会很有帮助,但总的来说,对于 GCC 和 MSVC++,情况如何?堆分配的性能不如堆栈分配吗?没有区别吗?或者差异如此之小以至于变得毫无意义的微优化。

我知道这很古老,但很高兴看到一些 C/C++ 片段展示了不同类型的分配。
你的奶牛工人非常无知,但更重要的是,他很危险,因为他对他非常无知的事情发表了权威性的声明。尽快从你的团队中剔除这些人。
请注意,堆通常比堆栈大得多。如果你分配了大量数据,你真的必须把它放在堆上,否则从操作系统改变堆栈大小。
除非您有基准或复杂性参数证明,否则所有优化都是默认无意义的微优化。
我想知道您的同事是否主要有 Java 或 C# 经验。在那些语言中,几乎所有东西都是在底层堆分配的,这可能会导致这样的假设。

c
ctuffli

堆栈分配要快得多,因为它真正所做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得相当的性能,但这会稍微增加复杂性并带来一些麻烦。

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


更重要的是,堆栈总是热的,你获得的内存比任何远堆分配的内存更有可能在缓存中
在一些(我知道的主要是嵌入式的)架构上,堆栈可能存储在快速的片上存储器(例如 SRAM)中。这可以产生巨大的影响!
因为堆栈实际上是一个堆栈。您不能释放堆栈使用的一块内存,除非它位于堆栈顶部。没有管理,你在上面推送或弹出东西。另一方面,堆内存是管理的:它向内核请求内存块,可能会拆分它们,合并它们,重用它们并释放它们。堆栈实际上是用于快速和短时间的分配。
@Pacerier 因为堆栈比堆小很多。如果要分配大数组,最好将它们分配在堆上。如果您尝试在堆栈上分配一个大数组,它会给您一个堆栈溢出。例如在 C++ 中试试这个: int t[100000000];尝试例如 t[10000000] = 10;然后 cout << t[10000000];它应该给你一个堆栈溢出或者只是不起作用并且不会显示任何东西。但是如果你在堆上分配数组: int *t = new int[100000000];并在之后执行相同的操作,它将起作用,因为堆具有如此大的数组所需的大小。
@Pacerier最明显的原因是堆栈上的对象在退出分配它们的块时超出范围。
P
Peter Hall

堆栈要快得多。它实际上只在大多数架构上使用一条指令,在大多数情况下,例如在 x86 上:

sub esp, 0x10

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

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

此外,几乎没有理由优化不需要验证的代码的性能,例如通过分析证明。 “过早的优化”通常会导致比其价值更多的问题。

我的经验法则:如果我知道我将在编译时需要一些数据,并且它的大小不到几百字节,我就会堆栈分配它。否则我堆分配它。


一条指令,通常由堆栈上的所有对象共享。
说得很好,尤其是关于可验证地需要它的观点。我不断地惊讶于人们对性能的担忧是如何错位的。
“解除分配”也很简单,只需一条 leave 指令即可完成。
请记住这里的“隐藏”成本,尤其是在您第一次扩展堆栈时。这样做可能会导致页面错误,上下文切换到内核需要做一些工作来分配内存(或从交换加载它,在最坏的情况下)。
在某些情况下,您甚至可以使用 0 指令来分配它。如果知道需要分配多少字节的一些信息,编译器可以在分配其他堆栈变量的同时提前分配它们。在这些情况下,您无需支付任何费用!
1
12 revs, 2 users 96%

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

#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。显然,优化编译器是许多程序员心目中的妖精。这个讨论曾经在答案的底部,但人们显然懒得读那么远,所以我把它移到这里以避免得到我已经回答过的问题。

一个优化编译器可能会注意到这段代码什么都不做,并可能把它全部优化掉。做这样的事情是优化器的工作,而与优化器抗争是愚蠢的差事。

我建议在关闭优化的情况下编译此代码,因为没有好的方法可以欺骗当前正在使用或将来使用的每个优化器。

任何打开优化器然后抱怨与它作斗争的人都应该受到公众的嘲笑。

如果我关心纳秒精度,我不会使用 std::clock()。如果我想将结果作为博士论文发表,我会在这方面做更多的事情,我可能会比较 GCC、Tendra/Ten15、LLVM、Watcom、Borland、Visual C++、Digital Mars、ICC 和其他编译器。实际上,堆分配比堆栈分配花费的时间要长数百倍,而且我认为进一步研究这个问题没有任何用处。

优化器的任务是摆脱我正在测试的代码。我看不出有任何理由告诉优化器运行然后试图欺骗优化器使其不实际优化。但如果我看到这样做的价值,我会做以下一项或多项:

将数据成员添加为空,并在循环中访问该数据成员;但是如果我只从数据成员中读取,优化器可以进行常量折叠并删除循环;如果我只写数据成员,优化器可能会跳过循环的最后一次迭代。此外,问题不是“堆栈分配和数据访问与堆分配和数据访问”。声明 e volatile,但 volatile 经常编译不正确 (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

此外,您应该在 main 函数的最开始添加一个“校准”循环,让您了解每个循环周期的时间,并调整其他循环以确保您的示例运行一些时间,而不是您使用的固定常数。
摆脱这样的代码是优化器的工作。是否有充分的理由打开优化器然后阻止它实际优化?我已经编辑了答案以使事情更加清晰:如果您喜欢与优化器作斗争,请准备好了解编译器编写者的聪明程度。
我很晚了,但这里也非常值得一提的是,堆分配通过内核请求内存,所以性能影响也很大程度上取决于内核的效率。将此代码与 Linux 一起使用(Linux 3.10.7-gentoo #2 SMP Wed Sep 4 18:58:21 MDT 2013 x86_64),修改 HR 计时器,并在每个循环中使用 1 亿次迭代会产生以下性能:stack allocation took 0.15354 seconds, heap allocation took 0.834044 seconds -O0 设置,使 Linux 堆分配仅在我的特定机器上减慢约 5.5 倍。
在没有优化(调试构建)的 Windows 上,它将使用比非调试堆慢得多的调试堆。我认为“欺骗”优化器根本不是一个坏主意。编译器编写者很聪明,但编译器不是人工智能。
微基准测试很难。您不能只禁用优化,因为这会给您带来不切实际的代码生成:例如,将循环计数器保存在内存中,这样您就可以在每 ~6 个时钟周期进行 1 次迭代,因为存储转发延迟。您肯定希望优化器优化您未测量的所有内容,并强制它执行您实际想要测量的工作。例如,将您的目标函数放在一个单独的文件中并禁用链接时优化,或者在函数上使用 [noinline]。您可能需要 volatile。你通常必须检查 asm 以确保你得到了你想要的东西。
F
Furious Coder

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

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


大多数多核机器都是如此,而不仅仅是 Xenon。甚至 Cell 也必须这样做,因为您可能在该 PPU 内核上运行两个硬件线程。
这是堆分配器(特别差)实现的效果。更好的堆分配器不需要在每次分配时都获得锁。
C
Chris Jester-Young

您可以为特定大小的对象编写一个非常高效的特殊堆分配器。然而,一般的堆分配器并不是特别高效。

我也同意 Torbjörn Gyllebring 关于对象预期寿命的观点。好点子!


这有时被称为平板分配。
F
FrankHB

特定于 C++ 语言的问题

首先,C++ 没有规定所谓的“堆栈”或“堆”分配。如果您谈论的是块作用域中的自动对象,它们甚至没有“分配”。 (顺便说一句,C 中的自动存储持续时间绝对不同于“已分配”;后者在 C++ 用语中是“动态”的。)动态分配的内存在空闲存储上,不一定在“堆”上,尽管后者通常是(默认)实现。

尽管根据 abstract machine 语义规则,自动对象仍会占用内存,但允许符合 C++ 的实现在可以证明这无关紧要时忽略这一事实(当它不改变程序的可观察行为时)。此权限由 ISO C++ 中的 the as-if rule 授予,这也是启用常规优化的一般条款(在 ISO C 中也有几乎相同的规则)。除了 as-if 规则之外,ISO C++ 还具有 copy elision rules 以允许省略特定的对象创建。因此省略了所涉及的构造函数和析构函数调用。结果,与源代码隐含的幼稚抽象语义相比,这些构造函数和析构函数中的自动对象(如果有的话)也被消除了。

另一方面,免费商店分配绝对是设计上的“分配”。在 ISO C++ 规则下,这样的分配可以通过调用 分配函数 来实现。但是,从 ISO C++14 开始,有 a new (non-as-if) rule 允许在特定情况下合并全局分配函数(即 ::operator new)调用。所以动态分配操作的一部分也可以像自动对象的情况一样是无操作的。

分配函数分配内存资源。可以使用分配器基于分配进一步分配对象。对于自动对象,它们是直接呈现的——虽然可以访问底层内存并用于为其他对象提供内存(通过放置new),但这作为自由存储没有多大意义,因为没有办法将资源转移到其他地方。

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

关于 C++ 的实现

C++ 没有公开具体化的激活记录或某种一流的延续(例如,著名的 call/cc),没有办法直接操作激活记录帧 - 实现需要将自动对象放置到其中。一旦与底层实现(“本机”不可移植代码,例如内联汇编代码)没有(不可移植的)互操作,则忽略底层的帧分配可能是非常微不足道的。例如,当被调用的函数被内联时,可以有效地将帧合并到其他帧中,因此无法显示什么是“分配”。

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

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

但无论如何,总的来说,确实,堆栈帧分配的成本明显低于调用分配函数来操作空闲存储(除非它被完全优化掉),它本身可以有数百个(如果不是数百万个:-) 操作来维护堆栈指针和其他状态。分配功能通常基于托管环境提供的 API(例如操作系统提供的运行时)。与为函数调用保存自动对象的目的不同,这种分配是通用的,因此它们不会像堆栈那样具有框架结构。传统上,它们从称为 heap(或多个堆)的池存储中分配空间。与“栈”不同,这里的“堆”概念并不表示所使用的数据结构; it is derived from early language implementations decades ago。 (顺便说一句,调用堆栈通常在程序/线程启动时由环境从堆中分配固定或用户指定的大小。)用例的性质使得从堆中分配和释放要复杂得多(比推送/弹出堆栈帧),并且几乎不可能通过硬件直接优化。

对内存访问的影响

通常的堆栈分配总是将新帧放在顶部,因此它具有相当好的局部性。这对缓存很友好。 OTOH,在免费存储中随机分配的内存没有这样的属性。从 ISO C++17 开始,<memory_resource> 提供了池资源模板。这种接口的直接目的是允许连续分配的结果在内存中靠近在一起。这承认了这样一个事实,即这种策略通常有利于当代实现的性能,例如对现代架构中的缓存友好。不过,这是关于 access 而不是 allocation 的性能。

并发

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

空间效率

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

堆栈分配的限制

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

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

其次,没有可靠且可移植的方法来检测堆栈空间耗尽。这通常被称为堆栈溢出(嗯,本网站的词源),但可能更准确地说,堆栈溢出。实际上,这通常会导致无效的内存访问,然后程序的状态就会被破坏(......或者更糟糕的是,一个安全漏洞)。事实上,ISO C++ 没有“堆栈”和makes it undefined behavior when the resource is exhausted 的概念。请注意应该为自动对象留出多少空间。

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

然而,有时需要深度递归调用。在需要支持未绑定的活动调用(调用深度仅受总内存限制)的语言实现中,不可能像典型的那样直接使用(当代)本机调用堆栈作为目标语言激活记录C++ 实现。为了解决这个问题,需要构建激活记录的替代方法。例如,SML/NJ 在堆上显式分配帧并使用 cactus stacks。这种激活记录帧的复杂分配通常不如调用堆栈帧快。但是,如果在 proper tail recursion 的保证下进一步实现此类语言,则对象语言中的直接堆栈分配(即语言中的“对象”不存储为引用,而是本地原始值,可以是一个-一对一映射到非共享的 C++ 对象)更复杂,通常会带来更多的性能损失。在使用 C++ 实现此类语言时,很难估计性能影响。


像 stl 一样,越来越少的人愿意区分这些概念。 cppcon2018 上的许多帅哥也经常使用 heap
@陈力“堆”可以清楚地记住一些特定的实现,所以有时可能还可以。不过,它“一般”是多余的。
什么是互操作?
@陈力我的意思是C ++源代码中涉及的任何类型的“本机”代码互操作,例如任何内联汇编代码。这依赖于 C++ 未涵盖的(ABI)假设。 COM 互操作(基于某些特定于 Windows 的 ABI)或多或少相似,尽管它与 C++ 基本无关。
这个答案的得分应该比它高得多。确实,there is no stack
M
MarkR

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

我强烈建议小件物品,以更适合分配范围的为准。对于大型项目,堆可能是必需的。

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

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


W
Windows programmer

通常堆栈分配只包括从堆栈指针寄存器中减去。这比搜索堆要快很多。

有时堆栈分配需要添加一页虚拟内存。添加零内存的新页面不需要从磁盘读取页面,因此通常这仍然比搜索堆快很多(特别是如果堆的一部分也被调出)。在极少数情况下,您可以构建这样一个示例,恰好在 RAM 中的部分堆中可用空间,但为堆栈分配新页面必须等待其他页面被写出到磁盘。在这种罕见的情况下,堆更快。


我不认为堆被“搜索”,除非它被分页。很确定固态存储器使用多路复用器并且可以直接访问存储器,因此是随机存取存储器。
这是一个例子。调用程序要求分配 37 个字节。库函数查找至少 40 字节的块。空闲列表上的第一个块有 16 个字节。空闲列表上的第二个块有 12 个字节。第三个块有 44 个字节。图书馆此时停止搜索。
J
Jay

除了与堆分配相比具有数量级的性能优势外,堆栈分配更适合长时间运行的服务器应用程序。即使是最好的托管堆最终也会变得如此碎片化,以至于应用程序性能下降。


l
larsivi

可能堆分配与堆栈分配的最大问题是,在一般情况下,堆分配是一个无界操作,因此您不能在时间有问题的情况下使用它。

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


y
yogman

堆栈的容量有限,而堆则没有。进程或线程的典型堆栈约为 8K。一旦分配,您就无法更改大小。

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

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


默认情况下,为现代操作系统上的用户模式堆栈分配的虚拟地址空间量可能至少为 64kB 或更大(Windows 上为 1MB)。您是在谈论内核堆栈大小吗?
在我的机器上,进程的默认堆栈大小是 8MB,而不是 kB。你的电脑几岁了?
M
MSalters

更快的不是 jsut 堆栈分配。您还可以在使用堆栈变量方面获益良多。它们具有更好的参考位置。最后,释放也便宜很多。


j
jakobengblom2

我认为生命周期至关重要,分配的东西是否必须以复杂的方式构建。例如,在事务驱动的建模中,通常需要填写并传入一个包含一堆字段的事务结构来操作函数。以 OSCI SystemC TLM-2.0 标准为例。

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

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

原因很简单,该对象具有昂贵的结构和相当长的使用寿命。

我会说:两者都试一下,看看哪种方法最适合你的情况,因为它真的取决于你的代码的行为。


A
Andrei Pokrovsky

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

一般来说,如果你关心性能,堆分配是你最后的选择。一个可行的中间选项可以是一个固定池分配器,它也只有几条指令,每次分配开销很小,因此非常适合小型固定大小的对象。不利的一面是,它仅适用于固定大小的对象,本质上不是线程安全的,并且存在块碎片问题。


w
wjl

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

但是,如果您的对象复制起来很昂贵,那么在您使用对象时如果不小心的话,在堆栈上分配可能会导致巨大的性能损失。

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

关键是,尽管在许多情况下堆栈分配通常比堆分配更好,但有时如果您在堆栈分配不适合计算模型时特意进行堆栈分配,它可能会导致比它解决的问题更多的问题。


M
Mike Dunlavey

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

您获得的优化与程序计数器实际在该代码中的时间量成正比。

如果您对程序计数器进行采样,您会发现它在哪里花费时间,这通常是在代码的一小部分中,并且通常在您无法控制的库例程中。

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


M
MSN

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

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

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


我不同意你的第一句话。
K
Kent Munthe Caspersen

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

现在举一个不能使用堆栈的例子:

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

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


S
Schemetrical
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 的内容,则不会发生推送。


N
Nikhil

之前已经提到,堆栈分配只是移动堆栈指针,即大多数体系结构上的一条指令。将其与堆分配情况下通常发生的情况进行比较。

操作系统以链表的形式维护部分空闲内存,其中有效负载数据由指向空闲部分起始地址的指针和空闲部分的大小组成。为了分配 X 字节的内存,遍历链表并依次访问每个音符,检查其大小是否至少为 X。当找到大小为 P >= X 的部分时,将 P 分成两部分尺寸 X 和 PX。更新链表并返回指向第一部分的指针。

如您所见,堆分配取决于您请求的内存量、内存的碎片程度等因素。


D
Dan Olson

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

做出这种区分可能会很好:您可以在堆上使用“堆栈分配器”。严格来说,我认为堆栈分配是指实际的分配方法,而不是分配的位置。如果您在实际的程序堆栈上分配了很多东西,那么由于各种原因,这可能会很糟糕。另一方面,尽可能使用堆栈方法在堆上分配是您可以为分配方法做出的最佳选择。

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


K
Ketan

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

如果您对应用程序很认真,那么 VTune 它或使用任何类似的分析工具并查看热点。

克坦


Z
ZijingWu

我想说实际上由 GCC 生成的代码(我也记得 VS)没有开销来进行堆栈分配。

说以下功能:

  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:

因此,无论您有多少局部变量(即使在 if 或 switch 内部),只有 3880 会更改为另一个值。除非你没有局部变量,否则这条指令只需要执行。所以分配局部变量没有开销。