C 和 C++ 如何在堆栈上存储大对象?

Posted

技术标签:

【中文标题】C 和 C++ 如何在堆栈上存储大对象?【英文标题】:How do C and C++ store large objects on the stack? 【发布时间】:2010-09-30 14:36:22 【问题描述】:

我试图弄清楚 C 和 C++ 如何在堆栈上存储大型对象。通常,堆栈是一个整数的大小,所以我不明白更大的对象是如何存储在那里的。它们只是占用多个堆栈“槽”吗?

【问题讨论】:

您必须明确“堆栈是整数的大小”的确切含义。你真的认为堆栈在 32 位架构上是 4 字节长吗?当您澄清时,请编辑您的问题而不是添加答案。 这就是他们只在学校教 Java 时发生的情况。乔尔是对的。 不,这不是一个坏问题,这表明当前的教育缺乏很多基础知识。至少,这家伙想知道哪个更适合他。 我同意,但问题是他们不再教那个了。它是所有高级语言,最好带有垃圾收集。有例外,但这些是我看到的信号。 我给他+1是因为他愿意学习。仅仅因为某人有误解,并不意味着他们应该被取笑。每个人都必须从某个地方开始,越早发现错误观念,对学习者越好。 【参考方案1】:

“堆栈是一个整数的大小”,你的意思是“堆栈指针是一个整数的大小”。它指向堆栈的顶部,这是一个巨大的内存区域。嗯,大于整数。

【讨论】:

【参考方案2】:

堆栈和堆并没有你想象的那么不同!


确实,某些操作系统确实存在堆栈限制。 (其中一些也有令人讨厌的堆限制!)

但这已经不是 1985 年了。

这些天,我在运行 Linux!

我的默认 stacksize 限制为 10 MB。我的默认 heapsize 是无限的。无限制堆栈大小非常简单。 (*cough* [tcsh] unlimit stacksize *cough*. 或者 setrlimit().)

stackheap最大的区别是:

    stack 分配只是偏移一个指针(如果堆栈变得足够大,可能会分配新的内存页面)。 必须搜索其数据结构以找到合适的内存块。 (也可能分配新的内存页。) stack 当前块结束时超出范围。 在调用 delete/free 时超出范围。 可能会变得支离破碎。 堆栈永远不会碎片化。

在Linux下,stackheap都是通过虚拟内存来管理的。

就分配时间而言,即使在严重碎片化的内存中进行堆搜索也无法映射到新的内存页面。 时间上的差异可以忽略不计!

根据您的操作系统,通常只有在您实际使用它们所映射的新内存页面时。(malloc() 分配期间NOT!)(这是一个懒惰的评估。)

new 会调用构造函数,它可能会使用那些内存页...)


您可以通过在 stackheap 上创建和销毁大型对象来破坏 VM 系统。系统是否可以/是否可以回收内存取决于您的操作系统/编译器。如果它没有被回收,堆可能能够重用它。 (假设在此期间它没有被另一个 malloc() 重新利用。)同样,如果堆栈没有被回收,它只会被重用。

虽然换出的页面需要换回,这将是您最大的打击。


在所有这些事情中,我最担心内存碎片

寿命(超出范围时)始终是决定因素。

但是当您长时间运行程序时,碎片会逐渐增加内存占用。不断的交换最终要了我的命!




修改添加:


伙计,我被宠坏了!

这里有些东西没有加起来……我认为要么*我*完全离谱。或者其他人都是。或者,更有可能的是,两者兼而有之。或者,只是也许,两者都不是。

无论答案是什么,我都必须知道发生了什么!

...这会很长。忍受我...


在过去的 12 年中,我大部分时间都在 Linux 下工作。大约 10 年前,在各种风格的 Unix 下。我对计算机的看法有些偏颇。我被宠坏了!

我在 Windows 上做了一些工作,但还不够权威。可悲的是,Mac OS/Darwin 也没有……尽管 Mac OS/Darwin/BSD 已经足够接近,以至于我的一些知识得以延续。


使用 32 位指针时,地址空间不足 4 GB (2^32)。

实际上,STACK+HEAP组合起来就是usually limited to somewhere between 2-4 GB as other things need to get mapped in there.

(有共享内存、共享库、内存映射文件、您运行的可执行映像总是很好等)


在 Linux/Unix/MacOS/Darwin/BSD 下,您可以在运行时人为地将 HEAPSTACK 限制为您想要的任意值。但最终会有一个硬系统限制。

这是 "limit""limit -h" 的区别(在 tcsh 中)。或者(在 bash 中)“ulimit -Sa” vs “ulimit -Ha”。或者,以编程方式,struct rlimit 中的 rlim_currlim_max


现在我们进入有趣的部分。关于Martin York 的准则。 (谢谢Martin!很好的例子。尝试一下总是好的!。)

Martin's 大概在 Mac 上运行。 (相当新的一个。他的编译器版本比我的要新!)

当然,他的代码默认不会在他的 Mac 上运行。但如果他首先调用 "unlimit stacksize" (tcsh) 或 "ulimit -Ss unlimited" (bash),它会运行得很好。


问题的核心:


在一个古老的(过时的)Linux RH9 2.4.x 内核盒上进行测试,分配大量 STACK   OR   HEAP,其中一个由本身最高在 2 到 3 GB 之间。 (遗憾的是,这台机器的 RAM+SWAP 的上限略低于 3.5 GB。它是一个 32 位操作系统。这不是唯一运行的进程。我们利用我们所拥有的...... )

所以在 Linux 下,STACK 大小和 HEAP 大小真的没有限制,除了人工的......


但是:


在 Mac 上,堆栈大小的硬性限制为 65532 KB。这与事物在内存中的布局方式有关。


Normally, you think of an idealized system as having STACK at one end of the memory address space, HEAP at the other, and they build towards each other. When they meet, you are out of memory.

Mac 似乎将它们的共享系统库固定在限制两侧的固定偏移量处。您仍然可以使用“无限堆栈大小”运行 Martin York 的代码,因为他只分配了 8 MiB (但他会在 HEAP 用完之前用完 STACK

我在 Linux 上。我不会。 Sorry kid. Here's a Nickel. Go get yourself a better OS.

There are workarounds for the Mac. But they get ugly and messy and involve tweaking the kernel or linker parameters.

从长远来看,除非 Apple 做一些真正愚蠢的事情,否则 64 位地址空间会在某个时候让整个堆栈限制变得过时。


继续碎片化:


任何时候你将一些东西推到 STACK 上,它都会被附加到最后。只要当前块退出,它就会被移除(回滚)。

因此,STACK 中没有漏洞。这都是一大块已用内存。最后可能只有一点未使用的空间,都可以重复使用。

相反,当 HEAP 被分配和释放时,你最终会出现未使用的内存漏洞。随着时间的推移,这些会逐渐导致内存占用增加。不是我们通常所说的核心泄漏,但结果是相似的。

内存碎片不是避免HEAP存储的原因。这只是您在编码时需要注意的事情。


这会带来 SWAP THRASHING


如果您已经分配/使用了大量堆。 如果你有很多零散的洞。 如果您有大量的小分配。

然后,您可以得到大量变量,所有变量都在代码的一个小的局部区域内使用,这些变量分散在大量的虚拟内存页面中。 (就像你在这个 2k 页面上使用 4 个字节,在那个 2k 页面上使用 8 个字节,等等很多页面......)

所有这些都意味着您的程序需要换入大量页面才能运行。或者它会不断地交换页面。 (我们称之为颠簸。)

另一方面,如果在 STACK 上进行了这些小分配,它们都将位于连续的内存段中。需要加载的 VM 内存页面更少。 (4+8+...

旁注:我提请注意这一点的原因源于我认识的一位电气工程师,他坚持将所有阵列分配在 HEAP 上。我们正在为图形做矩阵数学。一个 *LOT* 的 3 或 4 个元素数组。单独管理新/删除是一场噩梦。甚至在课堂上被抽象出来也会引起悲伤!


下一个话题。线程:


是的,默认情况下,线程仅限于非常小的堆栈。

您可以使用 pthread_attr_setstacksize() 更改它。虽然取决于您的线程实现,但如果多个线程共享相同的 32 位地址空间,单个线程堆栈将是一个问题!没有那么多空间!同样,过渡到 64 位地址空间 (OS) 会有所帮助。

pthread_t       threadData;
pthread_attr_t  threadAttributes;

pthread_attr_init( & threadAttributes );
ASSERT_IS( 0, pthread_attr_setdetachstate( & threadAttributes,
                                             PTHREAD_CREATE_DETACHED ) );

ASSERT_IS( 0, pthread_attr_setstacksize  ( & threadAttributes,
                                             128 * 1024 * 1024 ) );

ASSERT_IS( 0, pthread_create ( & threadData,
                               & threadAttributes,
                               & runthread,
                               NULL ) );

关于Martin York 的 Stack Frames:


也许你和我在想不同的事情?

当我想到一个堆栈框架时,我想到了一个调用堆栈。每个函数或方法都有自己的堆栈帧,由返回地址、参数和本地数据组成。

我从未见过堆栈框架的大小有任何限制。 STACK 整体上存在限制,但这是所有 堆栈帧 的组合。

There's a nice diagram and discussion of stack frames over on Wiki.


最后一点:


在 Linux/Unix/MacOS/Darwin/BSD 下,可以通过编程方式更改最大 STACK 大小限制以及 limit(tcsh) 或 ulimit(bash):

struct rlimit  limits;
limits.rlim_cur = RLIM_INFINITY;
limits.rlim_max = RLIM_INFINITY;
ASSERT_IS( 0, setrlimit( RLIMIT_STACK, & limits ) );

只是不要尝试在 Mac 上将其设置为 INFINITY...并在尝试使用它之前对其进行更改。 ;-)


延伸阅读:


http://www.informit.com/content/images/0131453483/downloads/gorman_book.pdf http://www.redhat.com/magazine/001nov04/features/vm/ http://dirac.org/linux/gdb/02a-Memory_Layout_And_The_Stack.php http://people.redhat.com/alikins/system_tuning.html http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html http://www.kegel.com/stackcheck/

【讨论】:

哇,你从哪里复制的:D? 来吧,说真的:这个答案是我在这里见过的最好和最有趣的答案之一。太糟糕了@Mr.Ree 自 12 年以来就不再存在了。【参考方案3】:

堆栈是一大块内存,用于存储局部变量、函数调用返回的信息等。堆栈的实际大小在操作系统上差异很大。例如,在 Windows 上创建新线程时,default size is 1 MB.

如果您尝试创建一个堆栈对象,该对象需要的内存比堆栈上当前可用的内存多,则会发生堆栈溢出,并且会发生一些不好的事情。一大类漏洞利用代码故意尝试创建这些或类似条件。

堆栈没有分成整数大小的块。它只是一个扁平的字节数组。它由 size_t 类型(不是 int)的“整数”索引。如果您创建一个适合当前可用空间的大型堆栈对象,它只会通过向上(或向下)堆栈指针来使用该空间。

正如其他人所指出的,最好将堆用于大型对象,而不是堆栈。这样可以避免堆栈溢出问题。

编辑:如果您使用的是 64 位应用程序并且您的操作系统和运行时库对您很好(请参阅 mrree 的帖子),那么在堆。如果您的应用程序是 32 位和/或您的操作系统/运行时库不是很好,您可能需要在堆上分配这些对象。

【讨论】:

嗯,堆栈限制取决于操作系统并且是人为的。我的(无限)堆栈大小与我的(无限)堆具有相同的内存大小限制。但是,由于生命周期和超出范围,堆栈不会像堆那样通过碎片增长。 在您的情况下使用堆会有更少的碎片风险。通过使用堆栈,您坚持必须在堆栈上分配内存。如果您有一个可动态调整大小的堆栈,则堆可以使用该空间或任何其他足够大的块。使用 RAII 进行范围删除。 另外,如果你有一个线程,它只能无限,因为所有线程共享相同的地址空间。 好消息! (虽然这取决于实现。)WRT 碎片,我需要 300 多个字符来响应。见:***.com/questions/429995/… @mrree:那一定是我见过的最长的 SO 帖子。我没有想过 64 位寻址。我同意,在可预见的未来,你会在地址空间用完之前很久就用完虚拟内存,除非你有一个荒谬的线程数和非常不均匀的堆栈使用率【参考方案4】:

堆栈是一块内存。堆栈指针指向顶部。可以将值压入堆栈并弹出以检索它们。

例如,如果我们有一个使用两个参数(1 个字节大小,另一个 2 个字节大小;假设我们有一台 8 位 PC)调用的函数。

两者都被压入堆栈,这将堆栈指针向上移动:

03: par2 byte2
02: par2 byte1
01: par1

现在调用函数并将返回地址放入堆栈:

05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1

好的,在函数中我们有 2 个局部变量; 2 个字节之一和 4 个字节之一。对于这些,堆栈上保留了一个位置,但首先我们保存堆栈指针,以便通过向上计数知道变量从哪里开始,通过向下计数找到参数。

11: var2 byte4
10: var2 byte3
09: var2 byte2
08: var2 byte1
07: var1 byte2
06: var1 byte1
    ---------
05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1

如您所见,只要您还有空间,您就可以将任何东西放入堆栈。否则,您将了解为该网站命名的现象。

【讨论】:

【参考方案5】:

每当您输入一个函数时,堆栈都会增长以适应该函数中的局部变量。给定一个使用 400 字节的 largeObject 类:

void MyFunc(int p1, largeObject p2, largeObject *p3)

   int s1;
   largeObject s2;
   largeObject *s3;

当您调用此函数时,您的堆栈将如下所示(细节会因调用约定和架构而异):

   [... rest of stack ...]
   [4 bytes for p1] 
   [400 bytes for p2]
   [4 bytes for p3]
   [return address]
   [old frame pointer]
   [4 bytes for s1]
   [400 bytes for s2]
   [4 bytes for s3]

有关堆栈如何操作的一些信息,请参阅x86 Calling Conventions。 MSDN 还为一些不同的调用对流提供了一些不错的图表,Sample Code 和 resulting stack diagrams。

【讨论】:

【参考方案6】:

正如其他人所说,不清楚你所说的“大物体”是什么意思......但是,既然你问了

它们是否只是占用多个堆栈 “插槽”?

我假设您只是指大于整数的任何东西。不过,正如其他人所指出的,堆栈没有整数大小的“槽”——它只是一段内存,其中的每个字节都有自己的地址。编译器通过该变量的 first 字节的地址跟踪每个变量——这是使用地址运算符 (&var) 时获得的值,以及 a指针就是其他变量的地址。编译器还知道每个变量是什么类型(你在声明变量时告诉它)​​,它知道每个类型应该有多大——当你编译程序时,它会做任何必要的数学运算来计算出这些变量有多少空间调用函数时需要的变量,并将结果包含在函数入口点代码中(PDaddy 提到的堆栈帧)。

【讨论】:

实际上,堆栈确实有​​插槽。您必须能够调用 int foo() int bar = 42;返回 *&bar; 。这意味着堆栈上的对象必须正确对齐,实际上是创建“槽”。一个插槽中存储的一半,另一个插槽中的一半未对齐。 确实,几乎所有非脑死编译器都会在字边界上对齐数据(包括填充结构以允许正确对齐),无论是在堆栈、堆还是静态数据中。不过,我个人不会将对齐描述为“槽”,而 ISTM 在这里这样做会掩盖的比它揭示的更多。【参考方案7】:

Pushpop 指令通常不用于存储本地堆栈帧变量。在函数的开头,堆栈帧是通过将堆栈指针递减函数的局部变量所需的字节数(与字长对齐)来建立的。这会为这些值“在堆栈上”分配必要的空间量。然后通过指向此堆栈帧的指针访问所有局部变量(x86 上的ebp)。

【讨论】:

【参考方案8】:

堆栈大小是有限的。通常堆栈大小是在创建进程时设置的。如果在 CreateThread() 调用中未另外指定,则该进程中的每个线程都会自动获取默认堆栈大小。所以,是的:可以有多个堆栈“槽”,但每个线程只有一个。而且它们不能在线程之间共享。

如果您将大于剩余堆栈大小的对象放入堆栈,则会发生堆栈溢出,您的应用程序将崩溃。

因此,如果您有非常大的对象,请将它们分配在堆上,而不是堆栈上。堆仅受虚拟内存量的限制(比堆栈大一个数量级)。

【讨论】:

嗯,堆栈限制取决于操作系统并且是人为的。我的(无限)堆栈大小与我的(无限)堆具有相同的内存大小限制。但是,由于生命周期和超出范围,堆栈不会像堆那样通过碎片增长。 是的,限制是人为的。但限制仍然存在,无论是由于操作系统还是碎片。 我需要 300 多个字符才能回复。见:***.com/questions/429995/…【参考方案9】:

在 C 和 C++ 中,您不应该在堆栈上存储大对象,因为堆栈是有限的(正如您所猜测的那样)。每个线程的堆栈通常只有几兆字节或更少(可以在创建线程时指定)。当您调用“new”来创建一个对象时,它不会被放入堆栈,而是被放入堆中。

【讨论】:

嗯,堆栈限制取决于操作系统并且是人为的。我的(无限)堆栈大小与我的(无限)堆具有相同的内存大小限制。但是,由于生命周期和超出范围,堆栈不会像堆那样通过碎片增长。 有趣的设置 mrree。然而,对于大多数操作系统和应用程序来说,堆栈大小限制是真实存在的。 我需要 300 多个字符才能回复。见:***.com/questions/429995/…【参考方案10】:

如何定义大对象?我们说的是大于还是小于分配的堆栈空间的大小?

例如,如果你有这样的事情:

void main() 
    int reallyreallybigobjectonthestack[1000000000];

根据您的系统,您可能会遇到段错误,因为根本没有足够的空间来存储对象。否则,它会像任何其他对象一样存储。如果您在实际物理内存中进行讨论,那么您不必担心这一点,因为操作系统级别的虚拟内存会负责处理。

堆栈的大小也不太可能是整数的大小,它完全取决于您的操作系统和应用程序的布局Virtual Address Space。

【讨论】:

如果 VM 使用惰性页面映射,则该分配可能不是问题。【参考方案11】:

您可以拥有足够大(或足够多)的对象,将它们放在堆栈上是没有意义的。在这种情况下,您可以将对象放在堆上并将指向它的指针放在堆栈上。这是按值传递和按引用传递的区别。

【讨论】:

以上是关于C 和 C++ 如何在堆栈上存储大对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何判断一个C++对象是不是在堆栈上

C++:如何在堆栈上创建对象数组?

在 C++ 中正确使用堆栈和堆?

C++ 对象存储在堆栈或堆中

跨语言的内存存储

堆栈缓冲区溢出(Windows,C++):如何检测罪魁祸首?