大型数组、std::vector和堆栈溢出

18

我有一个程序正在读取大数组中的数据,最初我在Visual Studio中将程序分成两个独立的项目,并且每个项目单独执行都没有问题,但是当我尝试将它们合并时,程序会跳过一些步骤并表现出异常行为。由于我对C++非常陌生,所以开始进行一些研究,并发现也许是由于那些巨大的数组填充了堆栈,所以我应该尝试将它们放到堆上。

我决定将每个数组更改为 std::vector,并用以下方式初始化:

std::vector<double> meanTimeAO = { 0.4437, 0.441, 0.44206, 0.44632, 0.4508, 0.45425,...}

但是在更改了所有数组之后,现在当我尝试编译时,编译器会因堆栈溢出而崩溃。我以为通过将数组更改为向量,我正在从堆栈中释放内存空间,但似乎结果相反。为什么会这样??

那么我应该如何处理这些大数组?(它们是固定的、永远不会改变的值或大小)


4
你尝试过将数组定义为static const float meanTimeAO[] = { 0.4437, 0.441, 0.44206, 0.44632, 0.4508, 0.45425, ... }吗?如果这样做,它们就不会被放在栈上。 - Cornstalks
1
将带有初始化程序的数组放在函数外部。 - M.M
解决所有数组固定大小的方法是使用称为链表的数据结构,其中每个元素存储一个对象,该对象引用下一个连续元素。或者您可以通过实例化新向量并复制所有元素(但这次具有更大的大小)来重新定义您的向量。 - Moshe Rabaev
好的,谢谢你们两个的解释,这让我明白了。作为一个相对新手的C++程序员,我对这个问题和答案有些困惑,现在我更好地理解他们的解决方案为什么很好。 - NonCreature0714
5
我很喜欢别人在 Stack Overflow 上问与 Stack Overflow 相关的问题。 - Michał Perłakowski
显示剩余4条评论
3个回答

11
如@Ajay的回答和@Cornstalks的评论所指出的那样,您可以通过在数组上使用staticconstexpr限定符来完全避免使用堆栈和堆。
const static std::array<float, 1000000> a1 = {}; // OK
constexpr    std::array<float, 1000000> a2 = {}; // OK in C++11 onwards

这将把数组存储在您的内存数据初始化部分(这里有很好的解释)。`const` 仅用于禁止修改 `a1`,并不需要避免堆栈溢出。声明为 `constexpr` 的变量也自动成为 `const`,因此不需要限定符。
注意:您还可以通过将数组作为全局变量来实现 `static` 的效果,但我不建议这样做。

程序堆栈溢出

如果您的数据是非静态的,并且元素数量非常大,则应使用 `std::vector`(或其他类型的堆分配内存)。
std::array<float, 1000000> a = {};   // Causes stack-overflow on 32-bit MSVS 2015
std::vector<float> v(1000000);       // OK

这是因为默认堆栈大小约为1MB,而100万个浮点数需要约4MB。堆的大小受系统可用内存(RAM)的限制。有关堆栈和堆的更多信息在此处std::vector 的缺点是它比 std::array 慢一些(堆内存分配、释放和访问速度都比堆栈慢),并且它的大小不是固定的。但是,您可以将 std::vector 声明为 const,以防止自己(或其他人)意外更改其大小或元素。
const std::vector<float> v = {...}; 

现在关于为什么您的std::vector会导致堆栈溢出有点神秘。然而,虽然std::vector将其元素分配在堆上,但它也在堆栈上分配指针(32位系统上为4字节,64位系统上为8字节)。因此,如果您同时拥有超过约250,000个std::vector,这也会导致堆栈溢出(或者64位系统上的约125,000个)。

编译器堆栈溢出

像任何程序一样,编译器也会分配内存 - 其中一些将在堆栈上。 MSVC上编译器堆栈溢出的官方错误是Fatal Error C1063

考虑到您的调试器表现异常,我的建议是通过手动将代码拆分为模块单元并逐个编译来隔离有问题的代码。可能会有少量的代码负责该错误,例如通过递归生成大量函数来占用大量堆栈。

或者,你的代码可能因为本质上过于复杂,自然需要比栈拥有的更多的内存。在这种情况下,拆分代码仍将有益,但你也可以尝试增加MSVC的默认堆栈大小

改进你的代码

要改进你的代码,你可以尝试将数据分成块。例如,你可以读取大约256 KB的数组,处理它,将数组写回文件,然后转移到下一个256 KB。你可以进一步选择块的大小小于L1缓存的大小(因此它可以一次性存储),这将通过最小化缓存未命中来提高性能。

注释

  1. MSVS 2015 (update 2) produces an internal compiler error when compiling

    #include "stdafx.h"
    #include <array>
    int main()
    {
         constexpr std::array<int, 1000000> a = {};
         return 0;
    }
    

    The static const variant works fine, and if I move a outside main (making it a global variable) then it also works fine.

  2. Not having a chkstk.asm is unusual. Mine is located at C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\i386\chkstk.asm. If you're missing it, then maybe try reinstalling MS Visual Studio.


我已经在VS2015上编译了相同的代码,并且编译通过。希望你也有RTM版本。此外,表格本身说明支持“constexpr”。 - Ajay
谢谢@Ajay!我刚刚在VS2015的新项目中测试了你的代码,它完美地工作了。我不知道以前在我的上一个项目中出了什么问题。对不起,我是指“扩展constexpr”不受支持(在“C++14核心语言特性”标题下)-但我认为我混淆了两者,因为我认为人们需要C ++14将变量声明为constexpr。 - Judge
啊,我找到了错误的源头。当我按原样测试你的代码时,它完美地工作了。然而,它只分配了6个浮点数(忽略了...)。当我尝试使用constexpr float a[1000000] = {};constexpr std::array<float, 1000000> a = {};时,我会收到“发生内部编译器错误”的提示,尽管我不确定为什么。对于小型数组,它可以正常工作。 - Judge
抱歉大家,我的错 - 我在主函数内初始化了数组。现在我已经将数组移出主函数,它完美地工作了。 - Judge
关于默认堆栈大小:1MB已经是很大的堆栈了(对吧,Windows?)。我的系统(Linux、BSD)大约只有8KB。 - Daniel Jour
@DanielJour 是的,那个数字是针对Windows上的Microsoft Visual Studio 2015特定的。我猜OP使用这个版本是因为他提到他的项目在Visual Studio中。你说得对,不同的系统有非常不同的堆栈大小(更多信息请参见这里)。 - Judge

9
如果数组的大小是固定的,并且其元素不会改变,那么就没有必要使用vector。你可以使用std::arrayconst 数组或constexpr数组代替。
constexpr float meanTimeAO[] = { 0.4437f, 0.441f, 0.44206f, 0.44632f, 0.4508f, 0.45425f,...}

2

在上面的答案中可能有一些被忽视的问题:

  1. "程序在调试时跳过了一些步骤"。楼主,你能否提供更多细节说明你的意思?也许,你正在调试一个发布版本,当你逐步执行代码时,你观察到正在执行的代码行不是你期望执行的下一行(这对于带有编译器优化的发布版本来说是完全正确的行为)。

  2. 问题说明了编译器出现了堆栈溢出而崩溃。不是执行的程序。所以问题是编译器问题。当然,改变代码可能会使编译器不崩溃,但上面关于在堆栈上分配std::vector的评论与可能导致编译器崩溃的原因无关。

建议:你可以尝试查看你正在使用的编译器版本是否存在已知的错误(即查看你的编译器供应商是否发布了更新版本,该版本可能解决了编译器崩溃的问题)。

此外,特别是针对你的“它们的值或大小永远不会改变”的评论,请将你的数据放入静态const double数组中,而不是std::vectors(甚至是链表)。一个不变的静态分配的只读链表在你应该只使用double[]时有点浪费时间。静态const数据在编译/链接时初始化,存在于自己的内存区域中(严格来说既不是堆栈也不是堆)。


非常感谢大家的回复...让我澄清一下我所说的第一点...当我调试程序时,如果我逐步执行“步入”,它会不断搜索一个不存在的名为chkstk.asm的文件。如果我在那个地方选择“步过”,程序就会继续运行,但是我的程序开头的所有初始化变量步骤都被跳过了。查看这个网站上的chkstk.asm是我得出我正在遇到堆栈满的问题的原因,这就是为什么我尝试将更大的数组更改为向量的原因。 - Diego Fernando Pava
关于第二点,编译器崩溃并显示错误信息:编译器达到堆栈溢出的限制。 - Diego Fernando Pava

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接