22

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

4

11 回答 11

35

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


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

但这不再是 1985 年了。

这些天,我运行 Linux!

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

最大的区别是:

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

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

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

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

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


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

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


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

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

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




修改为:


伙计,我被宠坏了!

只是这里没有加起来……我想要么*我*太离谱了。或者其他人都是。或者,更有可能的是,两者兼而有之。或者,只是也许,两者都不是。

不管答案是什么,我必须知道发生了什么事!

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


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

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


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

实际上,STACK + HEAP的组合通常限制在 2-4 GB 之间,因为其他东西需要在那里映射。

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


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

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


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

马丁大概在 Mac 上运行。(最近的一个。他的编译器版本比我的更新!)

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


事件的核心:


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

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


但:


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


通常,您将理想化的系统视为在内存地址空间的一端具有堆栈,在另一端具有,并且它们相互构建。当他们相遇时,你已经失去了记忆。

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

我在Linux上。我不会。 对不起孩子。这是镍。去给自己一个更好的操作系统。

Mac 有一些解决方法。但是它们变得丑陋和混乱,并涉及调整内核或链接器参数。

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


继续碎片化:


每当您将某些东西推到堆栈上时,   它都会附加到末尾。只要当前块退出,它就会被删除(回滚)。

结果,堆栈中没有孔。这都是一大块已用内存。最后可能只有一点未使用的空间,都可以重复使用。

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

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


这带来了SWAP THRASHING


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

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

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

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

旁注:我提请注意这一点的原因源于我认识的一位电气工程师,他坚持将所有阵列都分配在 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 的堆栈框架:


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

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

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

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 ) );

Just don't try to set it to INFINITY on a Mac... And change it before you try to use it. ;-)


Further reading:



于 2009-01-10T01:18:21.753 回答
29

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

例如,如果我们有一个使用两个参数调用的函数(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

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

于 2009-01-09T22:47:40.280 回答
9

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

于 2009-01-09T22:55:25.677 回答
5

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

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

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

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

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

于 2009-01-09T22:54:12.827 回答
3

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

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 调用约定。MSDN 还为一些不同的调用对流提供了一些不错的图表,包括示例代码生成的堆栈图

于 2009-01-09T22:52:46.230 回答
2

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

他们只是占用多个堆栈“插槽”吗?

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

于 2009-01-09T23:04:53.817 回答
1

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

于 2009-01-09T22:41:07.343 回答
1

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

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

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

于 2009-01-09T22:43:36.560 回答
0

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

于 2009-01-09T22:36:26.450 回答
0

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

于 2009-01-09T22:39:55.367 回答
0

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

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

void main() {
    int reallyreallybigobjectonthestack[1000000000];
}

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

此外,堆栈的大小不太可能是整数的大小,它完全取决于您的操作系统和应用程序虚拟地址空间的布局。

于 2009-01-09T22:39:56.457 回答