为什么不能修剪数组?

时间:2016-07-19 08:34:21

标签: c# arrays memory-management

在MSDN文档站点上,它说明了Array.Resize方法的以下内容:

  

如果newSize大于旧数组的长度,则为新数组   已分配并将所有元素从旧数组复制到   新的。

     

如果newSize小于旧数组的长度,则为新数组   已分配和元素从旧数组复制到新数组   直到新的填满;旧数组中的其余元素   被忽略了。

数组是一系列相邻的内存块。如果我们需要一个更大的阵列,我理解我们不能为它添加内存,因为它旁边的内存可能已经被其他一些数据声明了。所以我们必须要求一个具有所需更大尺寸的新的相邻内存块序列,复制我们的条目并删除我们对旧空间的主张。

但为什么要创建一个更小尺寸的新阵列?为什么数组不仅可以删除它对最后一个内存块的声明?然后它将是O(1)操作而不是O(n),就像现在一样。

是否与计算机架构或物理层面的数据组织有关?

7 个答案:

答案 0 :(得分:35)

未使用的内存实际上并未使用。任何堆实现的工作都是跟踪堆中的漏洞。管理者至少需要知道洞的大小,并需要跟踪他们的位置。这总是花费至少8个字节。

在.NET中,System.Object起着关键作用。每个人都知道它做了什么,在收集一个物体后它继续存在的事情并不那么明显。对象标题中的两个额外字段(同步块和类型句柄)然后变成指向前一个/下一个空闲块的向后和向前指针。它还具有最小大小,32位模式下12个字节。保证在收集对象后总是有足够的空间存储空闲块大小。

所以你现在可能已经看到了问题,减少数组的大小并不能保证创建一个足够大的空洞来适应这三个字段。没有什么可以做的,但抛出一个"不能这样做"例外。还取决于过程的位数。完全太难看了。

答案 1 :(得分:22)

回答你的问题,它与内存管理系统的设计有关。

理论上,如果你正在编写自己的记忆系统,你可以完全按照你所说的方式设计它。

然后问题就变成了为什么它没有这样设计。答案是内存管理系统在有效使用内存与性能之间进行了权衡。

例如,大多数内存管理系统都不会将内存管理到字节。相反,他们将内存分成8 KB的块。有很多原因可以解决这个问题。

部分原因与处理器移动内存的程度有关。例如,让我们说处理器在复制8 KB数据方面要好得多,然后复制4 KB。然后,以8 KB块的形式存储数据有一个性能优势。这将是基于CPU架构的设计权衡。

还有算法性能权衡。例如,通过研究大多数应用程序的行为,您会发现99%的应用程序分配大小为6 KB到8 KB的数据块。

如果内存系统允许你分配和发布4KB,那么将留下一个带有免费4KB块的内存,99%的分配将无法使用。如果不是过度分配到8 KB,即使只需要4 KB,它也会更加可重用。

考虑另一种设计。假设您有一个可用内存位置列表,可以是任意大小,并且请求分配2KB内存。一种方法是查看您的可用内存列表并找到一个大小至少为2KB的内存,但是您是否查看整个列表以找到最小的块,或者您确实找到了第一个足够大并使用的内存这一点。

第一种方法效率更高,但速度更慢,第二种方法效率更低但速度更快。

在C#和Java这样的语言中,它们会更加有趣,它们具有管理内存和#34;。在管理存储器系统中,存储器甚至没有被释放;它只是停止使用,垃圾收集器稍后会在稍后检测并释放。

有关不同内存管理和分配的更多信息,您可能需要查看Wikipedia上的这篇文章:

https://en.wikipedia.org/wiki/Memory_management

答案 2 :(得分:21)

我一直在寻找你问题的答案,因为我发现这是一个非常有趣的问题。我发现this answer有一个有趣的第一行:

  

你无法释放数组的一部分 - 你只能free()malloc()得到的指针,当你这样做时,你将释放你要求的所有分配。

所以实际上问题是寄存器会保留分配的内存。你不能只是释放你已分配的块的一部分,你必须完全释放它,或者你根本不释放它。这意味着为了释放内存,您必须先移动数据。我不知道.NET内存管理在这方面是否做了特别的事情,但我认为这条规则也适用于CLR。

答案 3 :(得分:6)

我认为这是因为旧数组没有被破坏。如果在其他地方引用它仍然可以访问它仍然存在。这就是在新的内存位置创建新数组的原因。

示例:

int[] original = new int[] { 1, 2, 3, 4, 5, 6 };
int[] otherReference = original; // currently points to the same object

Array.Resize(ref original, 3);

Console.WriteLine("---- OTHER REFERENCE-----");

for (int i = 0; i < otherReference.Length; i++)
{
    Console.WriteLine(i);
}

Console.WriteLine("---- ORIGINAL -----");

for (int i = 0; i < original.Length; i++)
{
    Console.WriteLine(i);
}

打印:

---- OTHER REFERENCE-----
0
1
2
3
4
5
---- ORIGINAL -----
0
1
2

答案 4 :(得分:5)

realloc的定义有两个原因:首先,它绝对清楚地表明不能保证调用较小大小的realloc会返回相同的指针。如果你的程序做出了这个假设,你的程序就会被破坏。即使指针在99.99%的时间内都是相同的。如果在大量空白空间的中间有一个大块,导致堆碎片,那么realloc可以自由地移动它,如果可能的话。

其次,有些实现绝对有必要这样做。例如,MacOS X有一个实现,其中一个大内存块用于分配1到16个字节的malloc块,另一个大内存块用于17到32个字节的malloc块,一个用于33到48个字节的malloc块等。这很自然地说,保持在范围内的任何大小更改(例如33到48个字节)都返回相同的块,但是更改为32或49个字节必须重新分配块。

无法保证realloc的性能。但实际上,人们并没有把尺寸缩小一点。主要情况是:将内存分配到所需大小的估计上限,填充它,然后调整大小到实际小得多的所需大小。或者分配内存,然后在不再需要时将其调整为非常小的内容。

答案 5 :(得分:3)

在任何堆管理系统中,可能有许多复杂的数据结构在“引擎盖下”运行。例如,他们可能会根据目前的大小存储块。如果允许块被“分割,增长和缩小”,它将添加 批次 的复杂性。 (而且,它确实不会让事情变得更快'。)

因此,实现会执行始终安全的事情:它会分配一个新块,并根据需要移动值。众所周知,“这种策略在任何系统上都能可靠地运行。”并且,它根本不会减慢速度。

答案 6 :(得分:2)

引擎盖下,数组存储在连续的内存块中,但仍然是许多语言的原始类型。

要回答您的问题,分配给数组的空间将被视为一个单独的块,如果是局部变量,则存储在stack中;如果是全局变量,则存储在bss/data segments。 AFAIK,当您访问低级别的array[3]数组时,操作系统将为您提供指向第一个元素的指针并跳转/跳过,直到达到所需的块(在上例中为三次)。 所以可能是一个架构决策,一旦声明了数组大小就无法更改。

类似地,OS在访问所需索引之前无法知道它是否是有效的数组索引。当它试图通过在jumping进程之后到达内存块来访问所请求的索引并发现到达的内存块不是数组的一部分时,它会抛出Exception