【问题标题】:Most efficient way to grow array C++增长数组 C++ 的最有效方法
【发布时间】:2015-12-11 10:24:09
【问题描述】:

抱歉,如果以前有人问过这个问题,我找不到一个完全回答我想知道的问题。他们提到了这样做的方法,但不比较方法。

我正在用 C++ 编写一个程序来解决 PDE 到稳定状态。我不知道这需要多少时间步骤。因此我不知道我的时间数组会有多长时间。这将有 100,000 秒的最大时间,但时间步长可能小至 0.001,因此在最坏的情况下它可能长达 1e8 doubles(也不一定是罕见的情况)。

就内存分配和运行时间而言,最有效的实现方式是什么?

我看过的选项:

  • 动态分配包含 1e8 个元素的数组,其中大部分元素永远不会被使用。
  • 最初分配一个较小的数组,在需要时创建一个较大的数组并复制元素
  • 使用std::vector 和它的大小增加功能

还有其他选择吗?

我主要关心速度,但我也想知道其中涉及到哪些内存考虑

【问题讨论】:

  • 我的 PDE 使用 C++ Boost 库中的 BLAS。考虑查看矩阵和稀疏矩阵类。
  • 具有 1e8 双精度的数组将占用多达 800 GB 的数据,这是您不希望拥有的。因此,您必须考虑更大的步长或更小的时间尺度。
  • 我实际上并没有进行计算。没想到会这么大:D
  • 您想针对一般情况、最常见情况还是最坏情况进行优化?使用std::vector 并根据球场猜测保留通常是一个好的开始。
  • @arc_lupus 1e8 是一亿,所以 ~800 MB。

标签: c++ arrays performance memory-management


【解决方案1】:

如果您担心速度,只需分配 1e8 双打就可以了。

在大多数情况下,矢量应该可以正常工作。请记住,附加的摊销是 O(1)。

除非您在非常奇怪的东西上运行,否则操作系统内存分配应该解决大多数碎片问题以及很难找到 800MB 空闲内存块这一事实。

【讨论】:

  • 如果您小心地以不调用双精度数的构造(归零)的方式进行分配,那么分配远远超过您将使用的量基本上是免费的。在您写入该页面之前,不会真正分配该内存的每一页。使用vector 并立即保留(不调整大小)到最大可能大小,行为方式相同。
  • 增长向量的摊销 O(1) 是正确的,但具有误导性。仅使用线性缓存效果,向量的增量(相对于准确的reserve)增长的成本将是简单算法的 2 倍。花费两倍的时间是 O(1),但通常不可接受。在这种情况下,缓存效果可能比线性更差,因此成本超过 2 倍(仍然是 O(1))
  • @JSF 我知道现代 linux 内核确实如此。您知道嵌入式应用程序、移动操作系统还是 Windows 是否适用?
  • 适用于 Windows。我最近看到的大多数嵌入式应用程序都使用精简的 Linux,但具有相同的内存管理。我实际上并不知道移动设备,但由于 CPU 很容易支持该功能,因此操作系统不支持该功能将是疯狂的。如果最初的问题是针对比现代风格弱得多的操作系统,那么问题应该是这样说的。
【解决方案2】:

了解您的机器上哪个选项“最有效”的唯一方法是尝试几个不同的选项和配置文件。我可能会从以下开始:

  1. std::vector 以最大可能大小构造。
  2. std::vector 采用保守的球场尺寸和 push_back。
  3. std::deque 和 push_back。

std::vector 与 std::deque 的辩论正在进行中。根据我的经验,当元素数量未知且不太大时,std::deque 几乎永远不会比std::vector 快(即使std::vector 需要多次重新分配)但最终可能会使用更少的内存。当元素数量未知且非常大时,std::deque 内存消耗似乎会爆炸式增长,std::vector 显然是赢家。

如果在分析之后,这些选项都不能提供令人满意的性能,那么您可能需要考虑编写自定义分配器。

【讨论】:

  • 自定义分配器根本没有帮助。
【解决方案3】:

如 cmets 中所述,如果您小心使用 vector,您实际上可以提前保留存储最大输入大小的容量(1e8 双倍),而无需在任何内存中分页。

为此,您要避免使用填充构造函数和resize 之类的方法(最终会访问所有内存)并使用reserve 和push_back 来填充它,并且只在需要时触摸内存。这将允许大多数操作系统一次简单地分页访问您访问的 vector 块,而不是一次全部内容。

然而,在这些类型的输入范围内,我倾向于避免这种解决方案,但原因很简单:

  1. 一种可能是偏执的可移植性担心我可能会遇到没有这种按需页面行为的操作系统。
  2. 可能是偏执的担心分配可能无法找到一组连续的未使用页面并面临内存不足错误(这是一个灰色区域 - 对于跨越千兆字节、数百兆字节的数组,我倾向于担心这一点是边界)。
  3. 只是一种完全主观的、可能是愚蠢/陈旧的偏见,即不要过于依赖操作系统在分配的内存中分页的行为,而是更喜欢具有简单按需分配的数据结构。
  4. 调试中。

在这四个中,前两个可能只是偏执狂。第三个可能只是愚蠢的。然而,至少在像 Windows 这样的操作系统上,当使用调试版本时,内存会在早期完全初始化,我们最终会立即将分配的页面映射到 DRAM,为这样的vector 保留容量。然后我们可能最终会导致轻微的启动延迟,并且任务管理器甚至在我们做任何事情之前就显示 800 MB 的内存使用量用于调试构建。

虽然通常调试构建的效率应该是次要问题,但当发布和调试之间的潜在差异很大时,它可能会开始导致生产代码几乎无法有效调试。因此,当差异可能像这样巨大时,我的偏好是“分块”。

我喜欢在这里应用的策略是分配更小的块——N 元素的更小的数组,其中N 可能是 512 个双精度数(刚好适合 4 KB 的公分母页面大小-- 可能会减去一些块元数据的双精度值)。我们用元素填充它们,当它们填满时,创建另一个块。

使用这些块,我们可以通过链接它们(形成展开列表)或将指向它们的指针向量存储在单独的聚合中来聚合它们,具体取决于是否需要随机访问或仅顺序访​​问就足够了。对于随机访问情况,这会产生轻微的开销,但我倾向于在这些输入规模上发现相对较小的情况,这些输入规模通常由内存层次结构的上层而不是寄存器和指令级别控制。

这对于您的情况来说可能是多余的,谨慎使用 vector 可能是最好的选择。但是,如果这还不够,并且您有与我类似的顾虑/需求,那么这种笨拙的解决方案可能会有所帮助。

【讨论】:

    猜你喜欢
    • 2021-11-11
    • 2010-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多