.NET运行时的设计者只能告诉您他们的实际推理。但是我的猜测是,内存安全在.NET中至关重要,并且维持内存安全和可变数组长度都非常昂贵,更不用说数组中任何代码的复杂程度了。
考虑简单的情况:
var fun = 0;
for (var i = 0; i < array.Length; i++)
{
fun ^= array[i];
}
为了维护内存安全,每个 array
访问都必须经过边界检查,同时确保边界检查不会被其他线程破坏(.NET运行时比C编译器有更严格的保证)。
因此,您需要一个线程安全的操作,该操作从数组中读取数据,同时检查边界。CPU上没有这样的指令,因此您唯一的选择是某种同步原语。您的代码变成:
var fun = 0;
for (var i = 0; i < array.Length; i++)
{
lock (array)
{
if (i >= array.Length) throw new IndexOutOfBoundsException(...);
fun ^= array[i];
}
}
不用说,这太贵了。使数组长度不变,将为您带来两个巨大的性能提升:
- 由于长度不能更改,因此边界检查不需要同步。这使得每个单独的边界检查便宜得多。
- ...,如果可以证明这样做的安全性,则可以省略边界检查。
实际上,运行时实际所做的最终类似于以下内容:
var fun = 0;
var len = array.Length;
for (var i = 0; i < len; i++)
{
fun ^= array[i];
}
您最终会陷入一个紧密的循环,这与C语言中的情况没什么不同-但同时,它是完全安全的。
现在,让我们看一下添加数组以所需方式缩小的利弊:
优点:
- 在极少数情况下,您想使数组更小,这意味着不需要复制数组即可更改其长度。但是,将来仍然需要进行堆压缩,这涉及大量复制。
- 如果将对象引用存储在数组中,如果数组和项目的分配恰好位于同一位置,则可以从缓存局部性中获得一些好处。不用说,这甚至比Pro#1稀有。
缺点:
- 即使在紧密的循环中,任何数组访问都将变得非常昂贵。所以每个人都会使用
unsafe
代码来代替代码,从而确保了内存的安全性。
- 处理数组的每一段代码都必须期望数组的长度可以随时更改。每个单个数组访问都需要一个
try ... catch (IndexOutOfRangeException)
,并且每个在数组上进行迭代的人都需要能够处理不断变化的大小-想知道为什么您不能List<T>
在迭代中添加或删除项吗?
- 对于CLR团队来说,大量的工作无法用在另一个更重要的功能上。
有一些实现细节,使它的好处更少。最重要的是,.NET堆与malloc
/free
模式无关。如果我们排除LOH,则当前MS.NET堆的行为将完全不同:
- 分配总是从顶部开始,就像在堆栈中一样。与相比,这使分配几乎与堆栈分配一样便宜
malloc
。
- 由于分配模式,要实际上“释放”内存,必须压缩在执行收集后堆。这将移动对象,以便填充堆中的可用空间,从而降低堆的“顶部”,从而使您可以在堆中分配更多对象,或者仅释放内存以供系统上的其他应用程序使用。
- 为了帮助保持缓存的局部性(假设经常一起使用的对象也彼此靠近分配,这是一个很好的假设),这可能涉及将堆中释放空间上方的每个对象向下移动。因此,您可能已经为自己保存了100字节数组的副本,但是无论如何您都必须移动100 MiB其他对象。
此外,正如汉斯在回答中很好地解释的那样,由于对象标头(请记住,.NET是如何为内存安全性吗?运行时必须知道对象的正确类型)。但是他没有指出的是,即使您确实有足够的内存,您仍然需要移动该数组。考虑一个简单的数组:
ObjectHeader,1,2,3,4,5
现在,我们删除最后两个项目:
OldObjectHeader;NewObjectHeader,1,2,3
哎呀。我们需要旧的对象标头来保留可用空间列表,否则我们将无法正确压缩堆。现在,可以完成将旧对象标头移到数组之外以避免复制的操作,但这又是一个麻烦。实际上,对于noöne永远不会使用的某些功能来说,这是相当昂贵的功能。
而这一切仍在托管的世界中。但是.NET的设计允许您在必要时使用不安全的代码-例如,在与非托管代码进行互操作时。现在,当您要将数据传递到本机应用程序时,有两个选择-固定托管句柄以防止其被收集和移动,或者复制数据。如果您正在进行简短的同步调用,则固定非常便宜(尽管更危险-本机代码没有任何安全保证)。例如,像在图像处理中那样,在紧密的循环中处理数据也是如此-复制数据显然不是一种选择。如果您允许Array.Resize
更改现有阵列,则将完全中断-因此Array.Resize
,则需要检查是否要与数组关联的句柄要调整大小,并在发生这种情况时引发异常。
更多的复杂性,难以想象的原因(跟踪仅偶尔发生一次的错误会很有趣,因为这种错误恰好会发生在Array.Resize
尝试重新调整数组大小的过程中,而这种错误现在恰好固定了记忆)。
正如其他人所解释的那样,本机代码并没有更好。尽管您不需要维护相同的安全性保证(我不会真正从中受益,但是,哦,好吧),但是分配和管理内存的方式仍然存在一些复杂性。叫做realloc
一个10个项目的5个项目的数组?好吧,要么复制它,要么仍然要保留10个项目的大小,因为无法以任何合理的方式回收剩余的内存。
因此,做一个简短的总结:您要的是一个非常昂贵的功能,在极其罕见的情况下,它的好处(如果有的话)非常有限,并且存在一个简单的解决方法(制作您自己的数组类) 。我看不出有没有通过“确定,让我们实现此功能!”的标准。:)
malloc
和和realloc
有时会realloc
移动对象,有时却不会。有时这样做的原因之一是内存管理器为小内存块提供了一个单独的池。将大内存块减少到小内存块可能需要将其移动到单独的池中。但是我不能说.NET是否相同。