如何在 C++ 中分配一个大的动态数组?

Posted

技术标签:

【中文标题】如何在 C++ 中分配一个大的动态数组?【英文标题】:How to allocate a large dynamic array in C++? 【发布时间】:2018-06-22 22:25:55 【问题描述】:

所以我目前正在尝试在 C++ 中动态分配大量元素(使用“new”)。显然,当“large”变得太大(>4GB)时,我的程序会因“bad_alloc”异常而崩溃,因为它找不到这么大的可用内存。

我可以分别分配数组的每个元素,然后将指向这些元素的指针存储在单独的数组中。但是,时间在我的应用程序中至关重要,因此我想尽可能避免缓存未命中。我也可以将其中一些元素组合成块,但这样的块的最佳大小是多少?

然后我的问题是:动态分配大型元素数组的最佳方式(时间)是什么,这样元素不必连续存储但必须可以通过索引访问(使用 [])? 这个数组永远不会被调整大小,不会插入或删除任何元素。

我认为我可以为此目的使用 std::deque,因为我知道 std::deque 的元素可能会或可能不会连续存储在内存中,但我读到有人担心这个容器需要额外的内存吗?

感谢您对此的帮助!

【问题讨论】:

询问您认为“合理”的最大块,如果发生这种情况,请抓住bad_alloc,将请求的大小减半。将此代码放入循环并重复,直到分配足够的内存。 我无法想象与 4GB 分配相比,std::deque 的内存开销会很大。 如果使用 Visual C++,您可能无法为 32 位进程分配超过 2GB 或为 64 位进程分配超过 4GB。您可能不需要那么多内存,需要重新考虑设计。 它是特定于操作系统和编译器的。也可能是处理器和ISA 特定的。你有多少内存?什么操作系统,你用什么编译器? @Ron Visual C++ 在分配 >4GB 内存块时没有问题,除非您竭尽全力将 64 位进程限制为 4GB 内存限制。 【参考方案1】:

如果你的问题是你实际上用尽了内存分配相当小的块(如双端队列所做的那样)并没有帮助,那么跟踪分配的开销只会使情况变得更糟。您需要重新考虑您的实现,以便您可以在仍然适合内存的块中处理它。对于此类问题,如果使用基于 x86 或 x64 的硬件,我建议至少 2 兆字节的块(大页面大小)。

【讨论】:

【参考方案2】:

显然,当“大”变得太大(>4GB)时,我的程序崩溃了 出现“bad_alloc”异常,因为它找不到这么大的块 可用内存。

此时您应该使用 64 位 CPU 和操作系统,分配巨大的连续内存块应该不是问题,除非您实际上内存不足。您可能正在构建 32 位程序。在这种情况下,您将无法分配超过 4 GB 的空间。您应该构建 64 位应用程序。

如果您想要比普通的 operator new 更好的东西,那么您的问题是特定于操作系统的。查看您的操作系统提供的 API:在 POSIX 系统上,您应该寻找 mmap,在 Windows 上寻找 VirtualAlloc

大分配存在多个问题:

出于安全原因,操作系统内核永远不会为您提供填充垃圾值的页面,而是将所有新内存初始化为零。这意味着您不必初始化该内存,只要零正是您想要的。 操作系统在第一次访问时为您提供真正的内存。如果您正在处理大型数组,您可能会浪费大量时间来处理页面错误。为避免这种情况,您可以在 Linux 上使用 MAP_POPULATE。在 Windows 上,您可以尝试PrefetchVirtualMemory(但我不确定它是否可以完成这项工作)。这应该会使 init 分配变慢,但应该会减少在内核中花费的总时间。 使用大块内存会浪费翻译后备缓冲区 (TLB) 中的插槽。根据您的内存访问模式,这可能会导致明显的减速。为避免这种情况,您可以尝试使用大页面(mmapMAP_HUGETLBMAP_HUGE_2MBMAP_HUGE_1GB 在 Linux 上、VirtualAllocMEM_LARGE_PAGES)。使用大页面并不容易,因为默认情况下它们通常不可用。它们也不能被换出(总是“锁定在内存中”),因此使用它们需要特权。

如果您不想使用特定于操作系统的功能,您可以在 C++ 中找到最好的方法是 std::calloc。与std::mallocoperator new 不同,它返回零初始化内存,因此您可以避免浪费时间初始化该内存。除此之外,该功能没有什么特别之处。但这是您在使用标准 C++ 时可以获得的最接近的结果。

没有设计用于处理大量分配的标准容器,而且,所有标准容器真的不擅长处理这些情况。

某些操作系统(如 Linux)会过度使用内存,而其他操作系统(如 Windows)则不会。如果 Windows 知道以后无法满足您的请求,它可能会拒绝给您内存。为避免这种情况,您可能需要增加页面文件。 Windows 需要预先在磁盘上保留该空间,但这并不意味着它会使用它(开始交换)。由于实际内存是延迟分配给程序的,因此可能会为应用程序保留大量内存,而这些内存永远不会真正分配给它们。

如果增加页面文件太不方便,可以尝试创建大文件并将其映射到内存中。该文件将作为您记忆的“页面文件”。请参阅CreateFileMappingMapViewOfFile

【讨论】:

【参考方案3】:

这个问题的答案非常依赖于应用程序和平台。现在,如果您只需要一个大于 4GB 的小整数因子,那么如果可能的话,您可以使用 64 位机器。有时也可以减小数组中元素的大小。 (例如,使用半浮点数的 16 位定点而不是 32 位浮点数。)

除此之外,您正在研究稀疏数组或核外技术。当您实际上并未在数组中的所有位置存储元素时,将使用稀疏数组。有许多可能的实现,哪一个最好取决于数据的分布和算法的访问模式。例如,请参阅Eigen。

核心外涉及到磁盘的显式读取和写入部分阵列。这曾经相当普遍,但现在人们非常努力地避免这样做。真正需要这样的应用程序通常构建在数据库或类似数据库之上以处理数据管理。在科学计算中,最终需要分配计算以及数据存储,因此也有很多复杂性。对于重要的问题,整个设计可能会受到良好的参考局部性的驱动。

任何稀疏数据结构都会在占用多少空间方面产生开销。这可能相当低,但这意味着如果您实际上有一个密集数组并且只是想避免内存碎片,则必须小心。

如果您的问题可以分解成较小的部分,一次只能访问数组的一部分,而主要问题是内存碎片使得分配一个大块变得困难,那么将数组分解成多个部分,有效地添加一个外部指针向量,是一个不错的选择。如果您可以随机访问大于 4 GB 的数组并且无法本地化访问,那么 64 位就是要走的路。

【讨论】:

【参考方案4】:

根据您需要的内存和速度问题,如果您使用的是 Linux,您可以随时尝试使用 mmap 并模拟一种交换。它可能会更慢,但您可以映射非常大的尺寸。见Mmap() an entire large file

【讨论】:

以上是关于如何在 C++ 中分配一个大的动态数组?的主要内容,如果未能解决你的问题,请参考以下文章

在函数 C++ 中分配内存二维数组

如何在计算方法和mapState中分配数组中的动态参数

如何使用指针在另一个函数中分配数组

在另一个类中分配对象数组(C++)

如何在 C++ 中分配类内数组的可修改大小

在函数 C 中分配内存二维数组