.NET中Generic和非泛型集合之间的内存使用差异

时间:2017-11-08 14:38:14

标签: c# .net generics memory collections

我现在读到.NET中的集合。众所周知,使用泛型集合优于非泛型有一些优点:它们是类型安全的,没有强制转换,没有装箱/拆箱。这就是通用集合具有性能优势的原因。

如果我们认为非泛型集合将每个成员存储为object,那么我们可以认为泛型也具有内存优势。但是,我没有找到任何有关内存使用差异的信息。

有人可以澄清这一点吗?

3 个答案:

答案 0 :(得分:18)

  

如果我们认为非泛型集合将每个成员存储为对象,那么我们可以认为泛型也具有内存优势。但是,我没有找到任何有关内存使用差异的信息。任何人都可以澄清这一点吗?

不确定。让我们考虑一个ArrayList,其中包含int s与List<int>。假设每个列表中有1000 int个。

在两者中,集合类型是数组周围的薄包装 - 因此名称为ArrayList。在ArrayList的情况下,基础object[]包含至少1000个盒装整数。对于List<int>,我们的基础int[]至少包含1000 int个。

为什么我说&#34;至少&#34;?因为两者都使用双倍完整策略。如果在创建集合时设置集合的容量,则会为该集合分配足够的空间。如果你没有,那么该集合必须猜测,如果它猜错了你需要更多的容量,那么它的容量就会增加一倍。所以,最好的情况是,我们的集合数组大小合适。最糟糕的情况是,它们可能是它们需要的两倍;数组中可能有2000个对象或2000个整数的空间。

但为了简单起见,我们假设我们很幸运,每个人大约有1000人。

首先,只是阵列的内存负担是什么? object[1000]在32位系统上占用4000个字节,在64位系统上占用8000个字节,仅用于指针大小的引用。无论int[1000]占用4000个字节。 (阵列簿记也占用了一些额外的字节,但与边际成本相比,这些成本很小。)

因此,我们已经看到非通用解决方案可能仅消耗两倍于阵列的内存。那个数组的内容怎么样?

嗯,关于值类型的事情是它们存储在它们自己的变量中。除了用于存储1000个整数的4000个字节之外,没有额外的空间;他们被打包到阵列中。因此,通用案例的额外成本为零。

对于object[]情况,数组的每个成员都是一个引用,该引用引用一个对象;在这种情况下,一个盒装整数。什么是盒装整数的大小?

未装箱的值类型不需要存储有关其类型的任何信息,因为其类型由其所在的存储类型以及运行时已知的类型决定。盒装值类型需要在某处存储框中事物的类型,并占用空间。事实证明,32位.NET中对象的簿记开销是8字节,64位系统上是16。这只是开销;我们当然需要4个字节用于int。但等等,情况变得更糟:在64位系统上,盒子必须与8字节边界对齐,因此我们需要在64位系统上另外 4字节的填充。

全部添加:我们的int[]在64位和32位系统上大约需要4KB。我们的object[]包含1000个int在32位系统上大约需要16KB,在64位系统上需要32K。因此,对于非一般情况,int[]object[]的内存效率要差4到8倍。

但等等,情况变得更糟。这只是大小。访问时间怎么样?

要从整数数组中访问整数,运行时必须:

  • 验证数组是否有效
  • 验证索引是否有效
  • 从给定索引处的变量中获取值

要从盒装整数数组中访问整数,运行时必须:

  • 验证数组是否有效
  • 验证索引是否有效
  • 从给定索引处的变量中获取引用
  • 验证引用是否为空
  • 验证引用是否为盒装整数
  • 从框中提取整数

这需要更多步骤,因此需要更长的时间。

但等待它会变得更糟。

现代处理器在芯片本身使用缓存以避免返回主存。 1000个普通整数的数组很可能最终进入高速缓存,以便快速连续地对数组的第一个,第二个,第三个等成员的访问全部从同一个高速缓存行中提取;这是疯狂的。但是盒装整数可以遍布整个堆,这会增加缓存未命中率,这会进一步降低访问速度。

希望这足以澄清你对拳击惩罚的理解。

非盒装类型怎么样?数组字符串列表与List<string>

之间是否存在显着差异?

这里的惩罚要小得多,因为object[]string[]具有相似的性能特征和内存布局。在这种情况下,唯一的额外惩罚是(1)在运行时之前不捕获您的错误,(2)使代码更难以阅读和编辑,以及(3)运行时类型检查的轻微惩罚。

答案 1 :(得分:2)

  然后我们可以认为仿制药也具有记忆优势

这种假设是错误的,它只适用于价值类型。所以考虑一下:

new ArrayList { 1, 2, 3 };

这将隐含地将每个整数转换为object(称为装箱),以便将其存储到ArrayList中。这会导致您的内存开销,因为object肯定比简单的int大。

对于参考类型没有区别,因为不需要装箱。

使用这一个或另一个不应该被驱动,既不是性能问题也不是内存问题。但是你应该问问自己你想对结果做些什么。特别是如果您在编译时知道集合中存储的类型,则没有理由通过使用正确的泛型类型参数将此信息放入编译过程中。

无论如何,你应该总是使用泛型集合而不是非泛型集合,因为提到的类型安全。

编辑:如果使用非泛型集合或通用版本,您的实际问题是毫无意义的:总是使用通用集合。但不是因为它的内存使用。见:

ArrayList a = new ArrayList { 1, 2, 3};

VS

List<object> a = new List<object> { 1, 2, 3 };

两个列表将占用相同数量的内存,虽然第二个列表是通用的。这是因为他们整数分成object。所以这个问题的答案与记忆无关。

另一种说法是参考类型根本没有记忆差异:

ArrayList a = new ArrayList { myInstance, anotherInstance }

VS

List<MyClass> a = new List<MyClass> { myInstance, anotherInstance }

将产生相同的记忆结果。然而,第二个更容易维护,因为您可以直接使用实例而不需要转换它们。

答案 2 :(得分:0)

让我们假设我们有这样的陈述:

int valueType = 1;

所以现在我们在堆栈上有一个值如下:

i = 1

现在考虑我们现在这样做:

object boxingObject = valueType;

现在我们在内存中存储了两个值,堆栈中valueType的引用和堆中的value 1

boxingObject

1

因此,在装箱值类型的情况下,将会有额外的内存使用情况,如Microsoft docs所述:

  

装箱值类型在堆上分配对象实例并将值复制到新对象中。

有关完整信息,请参阅此link