【问题标题】:C/C++ maximum stack size of program on mainstream OSesC/C++ 程序的最大堆栈大小
【发布时间】:2010-12-22 00:13:44
【问题描述】:

我想在 100 X 100 阵列上进行 DFS。 (说数组的元素代表图节点)因此,假设最坏的情况,递归函数调用的深度可以达到 10000,每个调用最多占用 20 个字节。那么这是否可行意味着是否存在stackoverflow的可能性?

C/C++ 中堆栈的最大大小是多少?

请为两者指定 gcc
1) Windows 上的 cygwin
2) Unix

一般限制是什么?

【问题讨论】:

  • 你知道不用递归就可以实现深度优先搜索吧?
  • 不,我不知道,请解释一下。
  • 我在回答中做了一个没有递归的 DFS 小例子

标签: c++ c stack


【解决方案1】:

(2020 年 9 月 26 日添加)

2009 年 10 月 24 日,as @pixelbeat first pointed out hereBruno Haible empirically discovered the following default thread stack sizes 用于多个系统。他说在多线程程序中,“默认线程堆栈大小是:”

- glibc i386, x86_64    7.4 MB
- Tru64 5.1             5.2 MB
- Cygwin                1.8 MB
- Solaris 7..10           1 MB
- MacOS X 10.5          460 KB
- AIX 5                  98 KB
- OpenBSD 4.0            64 KB
- HP-UX 11               16 KB

请注意,以上单位均为 MB 和 KB(以 1000 为基数),而不是 MiB 和 KiB(以 1024 为基数)。我已经通过验证 7.4 MB 的情况向自己证明了这一点。

他还表示:

32 KB 超出了您在多线程程序中可以安全地在堆栈上分配的空间

他说:

而 sigaltstack 的默认堆栈大小 SIGSTKSZ 是

  • 在某些平台上只有 16 KB:IRIX、OSF/1、Haiku。
  • 在某些平台上只有 8 KB:glibc、NetBSD、OpenBSD、HP-UX、Solaris。
  • 在某些平台上只有 4 KB:AIX。

布鲁诺

他编写了以下简单的 Linux C 程序来凭经验确定上述值。您可以立即在您的系统上运行它以快速查看您的最大线程堆栈大小是多少,或者您可以在 GDBOnline 上在线运行它:https://onlinegdb.com/rkO9JnaHD

解释:它只是创建一个新线程,以便检查线程堆栈大小而不是程序堆栈大小,在如果它们不同,那么它会使用Linux alloca() call 在堆栈(不是堆)上重复分配 128 个字节的内存,然后将 0 写入这个新的第一个字节内存块,然后打印出它分配的总字节数。它重复这个过程,每次在堆栈上再分配 128 个字节,直到程序因Segmentation fault (core dumped) 错误而崩溃。最后打印的值是您的系统允许的估计最大线程堆栈大小。

重要提示:alloca() 在堆栈上分配尽管这看起来像在堆上动态分配内存,类似于malloc() 调用,alloca() 不会动态分配到堆上。相反,alloca() 是一个专门的 Linux 函数,用于“伪动态”(我不确定我会如何称呼它,所以这是我选择的术语)直接分配 到堆栈上 就好像它是静态分配的内存。 alloca() 使用和返回的堆栈内存在 函数级别 范围内,因此“在调用 alloca()函数 返回其调用者时自动释放。 "这就是为什么每次完成for 循环迭代并且到达for 循环范围的末尾时,它的静态作用域不会退出并且alloca() 分配的内存不会被释放。有关详细信息,请参阅man 3 alloca。以下是相关引述(已添加重点):

描述
alloca() 函数在调用者的堆栈帧 中分配size 个字节的空间。当调用alloca()函数返回给它的调用者时,这个临时空间会自动释放。

返回值
alloca() 函数返回一个指向分配空间开头的指针。 如果分配导致堆栈溢出,则程序行为未定义。

这是 Bruno Haible 2009 年 10 月 24 日的节目,copied directly from the GNU mailing list here

同样,您可以run it live online here

// By Bruno Haible
// 24 Oct. 2009
// Source: https://lists.gnu.org/archive/html/bug-coreutils/2009-10/msg00262.html

// =============== Program for determining the default thread stack size =========

#include <alloca.h>
#include <pthread.h>
#include <stdio.h>
void* threadfunc (void*p) {
  int n = 0;
  for (;;) {
    printf("Allocated %d bytes\n", n);
    fflush(stdout);
    n += 128;
    *((volatile char *) alloca(128)) = 0;
  }
}

int main()
{
  pthread_t thread;
  pthread_create(&thread, NULL, threadfunc, NULL);
  for (;;) {}
}

当我使用上面的链接在 GDBOnline 上运行它时,每次运行它都会得到完全相同的结果,无论是 C 还是 C++17 程序。运行大约需要 10 秒左右。以下是输出的最后几行:

Allocated 7449856 bytes
Allocated 7449984 bytes
Allocated 7450112 bytes
Allocated 7450240 bytes
Allocated 7450368 bytes
Allocated 7450496 bytes
Allocated 7450624 bytes
Allocated 7450752 bytes
Allocated 7450880 bytes
Segmentation fault (core dumped)

因此,这个系统的线程堆栈大小约为 7.45 MB,正如上文提到的 Bruno (7.4 MB)。

我对程序进行了一些更改,主要是为了清晰起见,但也是为了提高效率,还有一点是为了学习。

我的更改总结:

  1. [学习] 我将 BYTES_TO_ALLOCATE_EACH_LOOP 作为参数传递给 threadfunc(),只是为了练习在 C 中传入和使用通用 void* 参数。

    1. 注意:这也是传递给pthread_create() 的回调函数(在我的例子中为threadfunc())所需的函数原型,正如the pthread_create() function 所要求的那样。请参阅:https://www.man7.org/linux/man-pages/man3/pthread_create.3.html
  2. [效率] 我让主线程休眠而不是浪费地旋转。

  3. [清晰] 我添加了更详细的变量名称,例如 BYTES_TO_ALLOCATE_EACH_LOOPbytes_allocated

  4. [清晰] 我改变了这个:

     *((volatile char *) alloca(128)) = 0;
    

    到这里:

     volatile uint8_t * byte_buff = 
             (volatile uint8_t *)alloca(BYTES_TO_ALLOCATE_EACH_LOOP);
     byte_buff[0] = 0;
    

这是我修改后的测试程序,它做的事情和布鲁诺的完全一样,甚至有同样的结果:

您可以run it online heredownload it from my repo here。如果您选择从我的 repo 本地运行它,这里是我用于测试的构建和运行命令:

  1. 构建并作为 C 程序运行:

     mkdir -p bin && \
     gcc -Wall -Werror -g3 -O3 -std=c11 -pthread -o bin/tmp \
     onlinegdb--empirically_determine_max_thread_stack_size_GS_version.c && \
     time bin/tmp
    
  2. 构建并作为 C++ 程序运行:

     mkdir -p bin && \
     g++ -Wall -Werror -g3 -O3 -std=c++17 -pthread -o bin/tmp \
     onlinegdb--empirically_determine_max_thread_stack_size_GS_version.c && \
     time bin/tmp
    

在线程堆栈大小约为 7.4 MB 的快速计算机上本地运行需要

这是程序:

// =============== Program for determining the default thread stack size =========

// Modified by Gabriel Staples, 26 Sept. 2020

// Originally by Bruno Haible
// 24 Oct. 2009
// Source: https://lists.gnu.org/archive/html/bug-coreutils/2009-10/msg00262.html

#include <alloca.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h> // sleep

/// Thread function to repeatedly allocate memory within a thread, printing
/// the total memory allocated each time, until the program crashes. The last
/// value printed before the crash indicates how big a thread's stack size is.
///
/// Note: passing in a `uint32_t` as a `void *` type here is for practice,
/// to learn how to pass in ANY type to a func by using a `void *` parameter.
/// This is also the required function prototype, as required by the
/// `pthread_create()` function, for the callback function (this function)
/// passed to `pthread_create()`. See:
/// https://www.man7.org/linux/man-pages/man3/pthread_create.3.html
void* threadfunc(void* bytes_to_allocate_each_loop)
{
    const uint32_t BYTES_TO_ALLOCATE_EACH_LOOP =
            *(uint32_t*)bytes_to_allocate_each_loop;

    uint32_t bytes_allocated = 0;
    while (true)
    {
        printf("bytes_allocated = %u\n", bytes_allocated);
        fflush(stdout);
        // NB: it appears that you don't necessarily need `volatile` here,
        // but you DO definitely need to actually use (ex: write to) the
        // memory allocated by `alloca()`, as we do below, or else the
        // `alloca()` call does seem to get optimized out on some systems,
        // making this whole program just run infinitely forever without
        // ever hitting the expected segmentation fault.
        volatile uint8_t * byte_buff =
                (volatile uint8_t *)alloca(BYTES_TO_ALLOCATE_EACH_LOOP);
        byte_buff[0] = 0;
        bytes_allocated += BYTES_TO_ALLOCATE_EACH_LOOP;
    }
}

int main()
{
    const uint32_t BYTES_TO_ALLOCATE_EACH_LOOP = 128;

    pthread_t thread;
    pthread_create(&thread, NULL, threadfunc,
                   (void*)(&BYTES_TO_ALLOCATE_EACH_LOOP));
    while (true)
    {
        const unsigned int SLEEP_SEC = 10000;
        sleep(SLEEP_SEC);
    }

    return 0;
}

示例输出(与 Bruno Haible 的原始程序的结果相同):

bytes_allocated = 7450240                                                                                                                                                        
bytes_allocated = 7450368                                                                                                                                                        
bytes_allocated = 7450496                                                                                                                                                        
bytes_allocated = 7450624                                                                                                                                                        
bytes_allocated = 7450752                                                                                                                                                        
bytes_allocated = 7450880                                                                                                                                                        
Segmentation fault (core dumped) 

【讨论】:

  • 感谢您提供此答案。我也在 Windows 上运行了 Bruno 的代码,对输出的确切含义有点困惑(Windows 没有给我一个段错误错误,只是关闭了控制台)。带有 MinGW 的 Windows 需要 #include &lt;malloc.h&gt; 而不是 #include &lt;alloca.h&gt;,所以这可能值得一提。另外,我们可以不只是抓住 seg 错误并吐出那个数字吗?
  • @Skewjo,感谢您提供的信息。帮助 Window 用户。你如何在 C 中捕获一个段错误? (我不是说不能——我只是不知道怎么做)。另外,当你说and spit that number out 时,that number 是什么意思? that number 不是最后一个打印值 + 128 吗?如果是这样,这会增加什么额外价值(意思是:我们为什么要这样做?)来捕捉 seg 错误,然后吐出最后打印的数字 + 128,而不是像已经完成的那样只查看最后打印的数字?
  • 程序栈大小其实就是主线程栈大小吧?我确实注意到主线程和新创建的工作线程(11169280 与 9315968)之间的堆栈大小存在差异,知道为什么会有这样的差异吗?
  • @baye, program stack size actually means the main thread stack size, right? 这也是我的假设,所以我认为是这样,但我不是 100% 确定。至于主线程和创建的(worker)线程的堆栈大小的差异,我不知道为什么会有差异,但存在差异并不让我感到惊讶。如果此问题尚不存在,请考虑发布您的代码并在新问题中询问此问题。如果您这样做,请在此处发布您的问题的链接,以便我们查看。
【解决方案2】:

线程堆栈通常更小。 您可以在链接时更改默认值, 或在运行时更改。 作为参考,一些默认值是:

  • glibc i386、x86_64: 7.4 MB
  • Tru64 5.1: 5.2 MB
  • Cygwin: 1.8 MB
  • Solaris 7..10: 1 MB
  • MacOS X 10.5: 460 KB
  • AIX 5: 98 KB
  • OpenBSD 4.0: 64 KB
  • HP-UX 11: 16 KB

【讨论】:

【解决方案3】:

我认为在 Visual Studio 中默认堆栈大小为 1 MB,因此在递归深度为 10,000 的情况下,每个堆栈帧最多可以有 ~100 个字节,这对于 DFS 算法来说应该足够了。

包括 Visual Studio 在内的大多数编译器都允许您指定堆栈大小。在某些(全部?)linux 风格上,堆栈大小不是可执行文件的一部分,而是操作系统中的环境变量。然后,您可以使用 ulimit -s 检查堆栈大小并将其设置为新值,例如 ulimit -s 16384

这是一个link,其中包含 gcc 的默认堆栈大小。

没有递归的 DFS:

std::stack<Node> dfs;
dfs.push(start);
do {
    Node top = dfs.top();
    if (top is what we are looking for) {
       break;
    }
    dfs.pop();
    for (outgoing nodes from top) {
        dfs.push(outgoing node);
    }
} while (!dfs.empty())

【讨论】:

  • 仅供参考,BFS 是相同的,只是您使用 FIFO 而不是堆栈。
  • 是的,或者在 STL 语言中使用带有 pop_front/push_back 的 std::deque
  • 您的带有堆栈结果的 DFS 将与递归版本不同。在某些情况下这无关紧要,但在其他情况下(例如在拓扑排序中)你会得到错误的结果
  • 是的,VS 的默认限制确实是 1MB。更多信息和设置不同值的方法可以在 Microsoft 文档中找到:msdn.microsoft.com/en-us/library/tdkhxaks(v=vs.140).aspx
  • 我更喜欢为此类算法使用显式堆栈数据结构,而不是递归,这样 1. 不依赖于系统堆栈的大小,2. 可以更改算法以使用不同的数据结构例如队列或优先队列,而不扔掉所有代码。
【解决方案4】:

我刚刚工作的时候栈用完了,它是一个数据库,它正在运行一些线程,基本上之前的开发人员在栈上扔了一个大数组,反正栈是低的。该软件使用 Microsoft Visual Studio 2015 编译。

即使线程已经用完堆栈,它仍然默默地失败并继续,它只是在访问堆栈上的数据内容时堆栈溢出。

我能给出的最好建议是不要在堆栈上声明数组——尤其是在复杂的应用程序中,尤其是在线程中,而是使用堆。这就是它的用途;)

另外请记住,在声明堆栈时它可能不会立即失败,而只会在访问时失败。我的猜测是编译器“乐观地”在 windows 下声明堆栈,即它会假定堆栈已被声明并且大小足够,直到它开始使用它,然后发现堆栈不存在。

不同的操作系统可能有不同的堆栈声明策略。如果您知道这些政策是什么,请发表评论。

【讨论】:

    【解决方案5】:

    我不确定您对矩形数组进行深度优先搜索是什么意思,但我假设您知道自己在做什么。

    如果堆栈限制是一个问题,您应该能够将递归解决方案转换为迭代解决方案,将中间值推送到从堆分配的堆栈上。

    【讨论】:

      【解决方案6】:

      是的,存在堆栈溢出的可能性。 C 和 C++ 标准没有规定诸如堆栈深度之类的东西,这些通常是环境问题。

      大多数体面的开发环境和/或操作系统都允许您在链接或加载时调整进程的堆栈大小。

      您应该指定您使用的操作系统和开发环境以获得更有针对性的帮助。

      例如,在 Ubuntu Karmic Koala 下,gcc 的默认值是 2M 保留和 4K 提交,但可以在链接程序时更改。使用ld--stack 选项来执行此操作。

      【讨论】:

      • @lex:没有一般限制。这取决于很多参数。
      • @paxdiablo: 保留和提交是什么意思?
      • Reserved 是要分配多少地址空间,committed 是要附加多少后备存储。换句话说,保留地址空间并不意味着当你需要的时候内存就在那里。如果您从不使用超过 4K 的堆栈,那么您就不会为其他 1.6M 浪费实际内存。如果要保证有足够的堆栈,则保留和提交应该是相同的。
      • @paxdiablo 2M - 4k 不是 1.6M。只是说。 (前 3 次我读到你的评论让我很困惑)
      • @griffin,感谢 3 年多来第一个发现这一点的人。我当然是指“其余部分”——我会避免使用实际数字,以免犯另一个可能的错误:-)
      【解决方案7】:

      依赖平台、依赖工具链、依赖ulimit、依赖参数……完全没有规定,有很多静态和动态属性可以影响它。

      【讨论】:

      • 没有“一般限制”。在 Windows 上,使用默认的 VC++ 链接器选项和默认的 CreateThread 行为,通常每个线程大约 1 MiB。在 Linux 上,用户不受限制,我相信通常没有限制(堆栈可以向下增长以占用几乎整个地址空间)。基本上,如果你不得不问,你不应该使用堆栈。
      • 在嵌入式系统上,您可能有 4k 或更少。在这种情况下,即使使用堆栈是合理的,您也必须询问。答案通常是耸耸肩。
      • 是的,在内核模式下也是如此。
      猜你喜欢
      • 2015-02-13
      • 2013-03-22
      • 1970-01-01
      • 1970-01-01
      • 2011-08-01
      • 2011-07-27
      • 2020-10-17
      • 2018-06-29
      相关资源
      最近更新 更多