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

How do C and C++ store large objects on the stack?

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


堆和堆并不像你想象的那样不同!

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

但现在已经不是1985年了。好的。

现在,我运行Linux!好的。

我的默认堆栈大小限制为10 MB。我的默认堆大小是无限的。取消那个堆栈大小的链接是非常简单的。(*咳嗽*[TCSH]取消限制堆叠大小*咳嗽*。或setrlimit())好的。

堆栈和堆之间最大的区别是:好的。

  • 堆栈分配只是偏移一个指针(如果堆栈足够大,则可能分配新的内存页)。堆必须搜索其数据结构以找到合适的内存块。(也可能分配新的内存页。)
  • 当当前块结束时,堆栈超出范围。调用delete/free时,堆超出范围。
  • 堆可能会碎片化。堆栈永远不会碎片化。
  • 在Linux下,堆栈和堆都通过虚拟内存进行管理。好的。

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

    取决于您的操作系统,通常只有当您实际使用这些新内存页时,它们才会被映射。(不是在malloc()分配期间!)(这是一个懒惰的评价。)好的。

    (new将调用构造函数,这可能会使用那些内存页…)好的。

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

    虽然换出的页面需要重新换入,这将是你最大的点击率。好的。

    在所有这些事情中,我最担心的是记忆碎片!好的。

    寿命(当它超出范围)总是决定因素。好的。

    但是,当您长时间运行程序时,碎片会逐渐增加内存占用。不断的交换最终杀了我!好的。修正后补充:伙计,我被宠坏了!

    只是这里没有什么东西…我想要么"我"就太离谱了。或者其他人都是。或者,更可能的是,两者都有。或者,也许,两者都不是。好的。

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

    …这会很长。容忍我…好的。

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

    我对窗户做了一点,但还不够权威。不幸的是,在Mac OS/Darwin的帮助下…尽管Mac OS/Darwin/BSD已经足够接近了,我的一些知识仍在继续。好的。

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

    实际上,堆栈+堆组合通常被限制在2-4GB之间,因为需要在其中映射其他内容。好的。

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

    在linux/unix/macos/darwin/bsd下,可以人为地将堆或堆栈约束到运行时需要的任意值。但最终有一个严格的制度限制。好的。

    这是"limit"与"limit-h"的区别(在tcsh中)。或者是"ulimit-sa"和"ulimit-ha"的对比。或者,以编程方式,在结构rlimit中使用rlim cur与rlim max。好的。

    现在我们开始有趣的部分。关于马丁·约克守则。(谢谢马丁!很好的例子。试一试总是好的!)好的。

    马丁大概在用Mac电脑跑步。(相当新的一个。他的编译程序比我的更新!)好的。

    当然,默认情况下他的代码不会在Mac上运行。但如果他第一次调用"unlimit stacksize"(tcsh)或"ulimit-ss unlimited"(bash),它将运行得很好。好的。问题的核心:

    在一个古老的(过时的)Linuxrh9 2.4.x内核盒上进行测试,分配了大量的堆栈或堆,其中一个本身就可以达到2到3 GB。(不幸的是,机器的RAM+交换容量在3.5GB以下。这是一个32位操作系统。这并不是唯一运行的进程。我们用现有的东西来做……)好的。

    因此,在Linux下,堆栈大小和堆大小没有任何限制,除了人工的限制之外…好的。但是:

    在Mac上,硬堆栈大小限制为65532千字节。这与记忆中事物的布局有关。好的。

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

    mac似乎把它们的共享系统库放在中间,固定的偏移量限制了两边。您仍然可以使用"unlimit stacksize"运行MartinYork的代码,因为他只分配了8mib(<64mib)的数据。但他很快就会从烟囱里跑出来。好的。

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

    有针对Mac的解决方案。但是它们会变得丑陋和混乱,并且涉及到调整内核或链接器参数。好的。

    从长远来看,除非苹果做了一些非常愚蠢的事情,否则64位地址空间将使整个堆栈限制在不久的将来变得过时。好的。移动到碎片:

    每当您将某个东西推到堆栈上时,它就会被附加到末尾。当当前块退出时,它就会被移除(回滚)。好的。

    因此,堆栈中没有孔。这都是一大块用过的内存。最后可能只有一点未使用的空间,可以重用。好的。

    相反,当分配堆并释放堆时,就会出现未使用的内存孔。随着时间的推移,这些会逐渐增加内存占用。不是我们通常所说的核心泄漏,而是结果相似。好的。

    内存碎片不是避免堆存储的原因。这只是在编码时需要注意的事情。好的。这就产生了互换振荡:

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

    然后,您可以得到大量的变量,这些变量都在代码的一个小的本地化区域中使用,这些变量分散在许多虚拟内存页上。(就像在这个2K页面上使用4个字节,在那个2K页面上使用8个字节,等等,对于整个页面…)好的。

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

    另一方面,如果在堆栈上进行了这些小的分配,那么它们都将位于一个连续的内存段中。需要加载的虚拟机内存页更少。(4 + 8 +…<2公里获胜。)好的。

    Sidenote: My reason for calling attention to this stems from a certain electrical engineer I knew who insisted that all arrays be allocated on the HEAP. We were doing matrix math for graphics. A *LOT* of 3 or 4 element arrays. Managing new/delete alone was a nightmare. Even abstracted away in classes it caused grief!

    Ok.

    下一个话题。线程:

    是的,默认情况下线程被限制在非常小的堆栈中。好的。

    您可以使用pthread_attr_setstacksize()更改它。不过,根据线程实现的不同,如果多个线程共享相同的32位地址空间,那么每个线程的大型单个堆栈将是一个问题!只是没有那么大的空间!同样,转换到64位地址空间(OS)将有所帮助。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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下,可以通过编程方式更改最大堆栈大小限制以及限制(tcsh)或ulimit(bash):好的。

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

    不要试图在Mac上设置为无穷大…在你尝试使用它之前先改变它。;-)好的。进一步阅读:

    • 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/

    好啊。


    堆栈是一段内存。堆栈指针指向顶部。可以将值推送到堆栈上并弹出以检索它们。

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

    两者都被推到堆栈上,这会将堆栈指针向上移动:

    1
    2
    3
    03: par2 byte2
    02: par2 byte1
    01: par1

    现在调用函数并将返回的addres放在堆栈上:

    1
    2
    3
    4
    5
    05: ret byte2
    04: ret byte1
    03: par2 byte2
    02: par2 byte1
    01: par1

    在函数中,我们有两个局部变量,一个是2字节,另一个是4字节。对于这些,在堆栈上保留了一个位置,但是首先我们保存堆栈指针,这样我们就可以知道变量从何处开始计数,并通过计数找到参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    如您所见,只要您还有空间,就可以在堆栈上放置任何内容。否则你会得到这个网站的名字的现象。


    Pushpop指令通常不用于存储本地堆栈帧变量。在函数开始时,堆栈帧是通过将堆栈指针减量为函数局部变量所需的字节数(与字大小对齐)来设置的。这将为这些值分配"堆栈上"所需的空间量。然后,通过指向该堆栈帧的指针(x86上的ebp)访问所有局部变量。


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

    如果您试图创建一个堆栈对象,该对象需要比当前堆栈上可用的内存更多的内存,则会发生堆栈溢出和坏事情。一大类利用代码有目的地试图创建这些或类似的条件。

    堆栈不分为整数大小的块。它只是一个简单的字节数组。它是由一个"整数"类型的大小_t(不是int)索引的。如果创建一个适合当前可用空间的大型堆栈对象,它只通过向上(或向下)碰撞堆栈指针来使用该空间。

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

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


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

    1
    2
    3
    4
    5
    6
    void MyFunc(int p1, largeObject p2, largeObject *p3)
    {
       int s1;
       largeObject s2;
       largeObject *s3;
    }

    调用此函数时,您的堆栈将类似于此(详细信息将根据调用约定和体系结构而变化):

    1
    2
    3
    4
    5
    6
    7
    8
    9
       [... 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也有一些很好的图,包括示例代码和生成的堆栈图。


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

    Do they simply take up multiple stack
    "slots"?

    我假设你的意思是任何大于整数的东西。不过,正如其他人所指出的,堆栈没有整数大小的"插槽"——它只是内存的一部分,其中的每个字节都有自己的地址。编译器根据该变量第一个字节的地址跟踪每个变量——这是使用运算符地址(&var时得到的值,指针的值就是其他变量的地址。编译器还知道每个变量是什么类型(在声明变量时告诉过它),它知道每个类型应该有多大——在编译程序时,它做任何必要的数学运算,以计算调用函数时这些变量需要多少空间,并将结果包含在函数项POIN中。T代码(pddy提到的堆栈帧)。


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

    如果您将大于剩余堆栈大小的对象放入堆栈中,将得到堆栈溢出,应用程序将崩溃。

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


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


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

    例如,如果您有类似的内容:

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

    根据您的系统,您可能会得到一个segfault,因为根本没有足够的空间来存储对象。否则它会像其他对象一样存储。如果您在实际的物理内存中交谈,那么您不必担心这一点,因为操作系统级别的虚拟内存将负责处理这一问题。

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


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


    "stack是整数的大小",意思是"stack指针是整数的大小"。它指向堆栈的顶部,这是一个巨大的内存区域。好吧,大于一个整数。