在 .net 中分配新数组的大 O 成本

Posted

技术标签:

【中文标题】在 .net 中分配新数组的大 O 成本【英文标题】:Big O cost of allocating a new array in .net 【发布时间】:2018-12-04 21:49:55 【问题描述】:

.Net 中分配数组的大 O 时间复杂度是多少?

我猜如果数组足够小以适合临时段,它应该是 O(1),但是随着 n 变大,找到足够的内存变得更加困难,因此它可能会改变。

此外,大对象堆也可能是碎片化的,因此如果 n 足够大以使数组适合 LOH,它可能不会是 O(1)。

【问题讨论】:

"但是随着 n 变大,找到足够的内存变得更加困难,因此它可能会改变。"但是整个空闲内存都在一个连续的块中。您似乎在考虑 C++ 内存模型,其中对象不会通过 GC 在内存中移动。与小对象堆不同,LOH 没有被压缩,因此该语句适用于它。 LOH 是否使用 malloc 样式的内存管理? 它不会压缩内存。我不确定它们有多么相似(或者甚至不同的 C/C++ 实现往往有多么不同)。 O(1) 其中第 1 次操作的成本可能相差很大,这确实是您可以对任何远程典型分配有用的最佳方法 嗯,数组需要进行零初始化。我想这也能让你得到 O(n)。 【参考方案1】:

一个新数组将被分配到两个不同的堆中,大多数人可能都知道,这取决于它的大小(大小阈值为 85000 字节):

小对象堆 - 这里的分配发生在所谓的分配上下文中,它是内存的预置零区域,位于临时段内。这里可能会发生两种情况: 在当前分配上下文中有足够的空间用于新数组 - 在这种情况下,我们可以将其视为 O(1) 操作,仅返回数组的地址(并为下一个对象碰撞指针) 那里没有足够的空间 - 如果可能(就像它位于临时段的末尾),将尝试通过 分配量子(通常约为 8kB)来扩大分配上下文.在这里,我们达到了将这些 8kB 归零的成本,因此它要大得多。更糟糕的是,分配上下文可能无法扩大——因为它可能位于已分配的对象之间。在这种情况下,将创建一个新的分配上下文——在空闲列表的帮助下,在临时段内的某个地方,以利用碎片。在这种情况下,成本甚至更大——遍历空闲列表以找到合适的位置,然后将其归零。尽管如此,成本并不直接取决于数组大小并且是“恒定的”,因此我们可以像以前一样将其视为 O(1)。 大型对象堆 - 因为这里的分配默认频率要低得多,所以它使用“临时”分配上下文 - 每次在这里发生分配时,GC 都会在空闲列表的帮助下搜索适当的位置并将其归零.同样,空闲列表遍历和内存归零的成本都发生在这里,但由于这里的对象很大,它主要由归零成本主导。在这里我们可以谈谈 O(n) 成本。

在 LOH 分配的情况下,应该注意额外的隐藏“成本” - 在后台 GC 的某些部分期间不会发生此类分配(因为两者都在空闲列表上运行)。因此,如果碰巧有很多长的后台 GC,LOH 分配将暂停,等待 GC 结束。这显然会给您的线程带来不必要的延迟。

【讨论】:

“因为它可能位于已分配的对象之间” - 只有当有固定对象时才会发生这种情况,对吧?否则它应该被压缩。 您是否恰好有一个良好的合格/官方链接到您提供的条款(例如分配量)和数字(8k)?也许也适用于内部用于空闲列表等的数据结构?我没有找到好的。 一般而言,SOH 生成有时只是扫过,没有压缩。在这种情况下,分配上下文可能会重用空闲空间(碎片)。同意 - 它可能很少发生,因为 gen0/1 经常被压缩。并且固定总是可能“帮助”碎片化。 官方链接是BOTR,但它只是概括地解释它。您将在我的 Pro .NET 内存管理一书中找到更多详细信息,由 Maoni 审阅,因此可以视为半官方;)【参考方案2】:

临时段(SOH;小对象堆)中的对象在该段上的最后一个已知对象之后分配。它应该只是一个指向那里的指针。

不会考虑中间的“空白”空间,因为没有空白空间。即使对象不再有引用,它仍然会在那里,直到它被垃圾收集。然后,SOH 将被压缩,因此再次没有空闲空间。

如果 SOH 不够大,则必须选择不同的 SOH,或者必须创建新的段。这需要更长的时间,但仍然是 O(1)。

LOH 有点复杂,因为它通常不会被压缩。有些网站声明 LOH 有一个“免费列表”。但我不确定它是否真的是一个列表样式的实现。我猜它有更好的管理和像字典一样工作,所以应该不会比O(log(n))差。

需要做什么?

可能从内核获取新内存。如果是这样,则不需要内存was already zeroed 和 memset()。 如果新内存在 RAM 中不可用,请先将某些内容交换到磁盘。这部分可能会变得非常昂贵但不可预测。 如果内存在 .NET 中已经可用,则可能需要将其初始化为零。但是memset() is optimized 的实现(例如使用rep stos) 用某处(例如文件)中的值初始化数组。这很可能是一个 .NET 循环,除了交换其中一个昂贵的部件。

通常,我不会考虑内存分配问题,除非您使用了可以告诉您内存吞吐量问题的分析器(如 dotMemory)。相信 Donald Knuth:“过早的优化是万恶之源”。

【讨论】:

谢谢。这听起来很有意义。一般来说,即使在 LOH 上,分配数组的成本与使用值初始化它相比是微不足道的吗? 谢谢。实际情况是,我正在编写一种方法,由于列表复制,最坏情况下的性能很糟糕,我正在尝试对其进行优化。现在我正在使用一些计算来计算如何最好地最大化性能,但我假设复制一个列表是 O(n)。但后来我心想——那是为了抄袭。首先分配数组的成本是多少? @YairHalberstadt - 当涉及到性能问题时,GC 比分配更有可能是罪魁祸首。在非桌面平台上更是如此。 嗨@thomas-weller,你在这里和那里混合了一些东西。例如,对于 SOH,“不会考虑中间的“空白”空间,因为没有空白空间。”不完全正确。我在回答中提供了一些更详细的信息。

以上是关于在 .net 中分配新数组的大 O 成本的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 c++ 中分配 char 数组元素时,分配的字符被破坏?

所有javascript数组和对象方法的大O [关闭]

当用作哈希时,JavaScript 的数组的大 O 是多少?

JVM的内存分配策略

如何在ios中分隔数组

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