主流操作系统上 C/C + + 程序的最大堆栈大小

我想在100X100数组上做 DFS。(假设数组元素表示图形节点)所以假设最坏的情况,递归函数调用的深度可以达到10000,每次调用需要20个字节。那么,这种方法是否可行,是否存在堆栈溢出的可能性?

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

请为两者指定 gcc
1) Cygwin on Windows
2) Unix

一般的限制是什么?

189029 次浏览

平台依赖,工具链依赖,极限依赖,参数依赖..。它完全没有指定,而且有许多静态和动态属性可以影响它。

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

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

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

例如,在 Ubuntu Karmic Koala 下,gcc 的默认设置是2M 保留和4K 提交,但是在链接程序时可以更改这一设置。使用 ld--stack选项来实现这一点。

在 Visual Studio 中,默认的堆栈大小是1MB,所以递归深度为10,000时,每个堆栈帧最多可以大约100字节,这对 DFS 算法来说应该足够了。

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

下面是具有 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())

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

  • Glibc i386,x86 _ 64: < em > 7.4 MB
  • Tru645.1: < em > 5.2 MB
  • Cygwin: < em > 1.8 MB
  • Solaris 7. . 10: < em > 1 MB
  • MacOS X 10.5: < em > 460KB
  • AIX 5: < em > 98 KB
  • OpenBSD 4.0: < em > 64KB
  • HP-UX11: < em > 16KB

我不知道您所说的对矩形数组进行深度优先搜索是什么意思,但我想您知道自己在做什么。

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

我只是在工作中用完了堆栈,它是一个数据库,它正在运行一些线程,基本上前面的开发人员在堆栈上抛出了一个大数组,而堆栈反正是低的。该软件是使用 MicrosoftVisualStudio2015编译的。

即使线程已经用完了堆栈,它也会无声无息地失败并继续运行,只有在访问堆栈上数据的内容时才会堆栈溢出。

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

还要记住,在声明堆栈时,它可能不会立即失败,但只会在访问时失败。我的猜测是,编译器在 windows 下“乐观地”声明堆栈,也就是说,它会假设堆栈已经被声明,并且有足够的大小,直到它开始使用它,然后发现堆栈不在那里。

不同的操作系统可能有不同的堆栈声明策略。如果您知道这些策略是什么,请留下注释。

(由2020年9月26日起)

2009年10月24日,几个系统的 正如@pixelbeat 首先在这里指出的Bruno Haible 根据经验发现了以下默认线程堆栈大小。他说 在多线程程序中,“默认的线程堆栈大小是”是这样的。我在“实际”一栏中添加了大小,因为@Peter。然而,Cordes 在我的回答下面的评论中指出,下面显示的奇数测试数字并不包括所有的线程堆栈,因为其中一些是在初始化时使用的。如果我运行 ulimit -s查看我的 Linux 计算机配置的“最大堆栈大小”,它输出 8192 kB,正好是 8MB没有,下表中列出的我的 x86-64计算机使用 gcc 编译器和 glibc 的奇数 7.4 MB。因此,您可以对下表中的数字添加一些内容,以获得给定线程的实际完整堆栈大小。

请注意,下面的“测试”列单位都是 MB 和 KB (以1000为基数) ,而不是 MiB 和 KiB (以1024为基数)。我已经通过验证7.4 MB 的用例向自己证明了这一点。

Thread stack sizes


System and std library  Tested  Actual
----------------------  ------  ------
- glibc i386, x86_64    7.4 MB  8 MiB (8192 KiB, as shown by `ulimit -s`)
- 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  ?

Bruno Hable 还指出:

32KB 超出了在多线程程序中在堆栈上安全分配的容量

他说:

SIGSTKSZ 的默认堆栈大小为

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

布鲁诺

他编写了下面这个简单的 LinuxC 程序,通过经验来确定上面的值。现在,您可以在系统上运行它,以快速查看最大线程堆栈大小,或者您可以在 GDBOnline 上在线运行它,请点击: https://onlinegdb.com/rkO9JnaHD

说明: 它只是创建了一个单独的新线程,以便检查 线程堆栈大小线程堆栈大小和非 程序堆栈大小程序堆栈大小,如果它们不同,然后它让那个线程重复分配128字节的内存 在堆栈上(不是堆),使用 Linux alloca()调用,然后它写一个0到这个新内存块的第一个字节,然后它打印出它已经分配了多少总字节。它重复这个过程,每次多分配128个字节 在堆栈上,直到程序因为 Segmentation fault (core dumped)错误而崩溃。最后打印的值是系统允许的估计最大线程堆栈大小。

重要提示: alloca()分配 malloc()0: 即使这个 malloc()1动态内存分配到堆上,类似于 malloc()调用,alloca()也不会动态分配到堆上。相反,alloca()是一个特殊的 Linux 函数,用于“伪动态”(我不确定我怎么称呼它,所以我选择了这个术语)直接分配 malloc()2,就好像它是静态分配的内存一样。由 alloca()使用和返回的堆栈内存的作用域是 malloc()3,因此“当调用 alloca()malloc()4返回给其调用者时自动释放”这就是为什么每次完成 for循环迭代并到达 for循环范围结束时,它的静态范围不会退出,alloca()分配的内存也不会被释放。详情请参阅 malloc()5。下面是相关的引用(强调后加) :

描述
alloca()函数在 调用程序的堆栈帧中分配 尺寸字节的空间。当向其调用者调用 alloca() 报税表功能时,将自动释放此临时空间。

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

以下是布鲁诺 · 海布尔2009年10月24日的节目:

同样,你可以 在线直播

// 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()) ,这也是 pthread_create()函数所需的函数原型。见: 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;
    

下面是我修改后的测试程序,它和布鲁诺的程序完全一样,甚至得到了相同的结果:

你可以选择 在网上查一下或者 从我的回收站下载。如果您选择从我的回购本地运行它,下面是我用于测试的构建和运行命令:

  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 的快速计算机上本地运行需要 < 0.5秒。

程序是这样的:

// =============== 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;
}

样本输出(与布鲁诺 · 海布尔的原始程序相同的结果) :

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