如果我们考虑到非泛型集合将每个成员存储为对象,那么我们可以认为泛型也具有内存优势。但是,我没有找到任何有关内存使用差异的信息。有人能澄清这一点吗?
当然。让我们考虑一个ArrayList
其中包含int
s vs a List<int>
。假设有 1000 个int
每个列表中都有 s。
在这两种情况下,集合类型都是数组的薄包装——因此得名ArrayList
。如果是ArrayList
,有一个底层的object[]
包含至少 1000 个装箱整数。如果是List<int>
,有一个底层的int[]
至少包含 1000 个int
s.
为什么我说“至少”?因为两者都采用双倍满策略。如果您在创建集合时设置了集合的容量,那么它会为很多东西分配足够的空间。如果你不这样做,那么集合必须猜测,如果它猜测错误并且你需要更多容量,那么它的容量就会增加一倍。因此,最好的情况是,我们的集合数组的大小完全正确。最坏的情况是,它们的大小可能是所需大小的两倍;数组中可以容纳 2000 个对象或 2000 个整数。
但为了简单起见,我们假设我们很幸运,每个都有大约 1000 个。
首先,数组的内存负担是多少?一个object[1000]
仅用于引用,在 32 位系统上占用 4000 字节,在 64 位系统上占用 8000 字节,指针大小。一个int[1000]
无论如何占用4000字节。 (数组簿记还占用了一些额外的字节,但与边际成本相比,这些成本很小。)
因此,我们已经看到非通用解决方案可能会消耗两倍于数组的内存。那么数组的内容呢?
嗯,关于值类型的事情是它们存储在自己的变量中。除了用于存储 1000 个整数的 4000 字节之外,没有额外的空间;它们被直接打包到数组中。因此,对于一般情况,额外成本为零。
For the object[]
在这种情况下,数组的每个成员都是一个引用,并且该引用引用一个对象;在本例中,是一个装箱整数。装箱整数的大小是多少?
未装箱的值类型不需要存储有关其类型的任何信息,因为其类型由其所在存储的类型确定,并且运行时已知这一点。装箱值类型需要在某个地方存储框中事物的类型,这会占用空间。事实证明,32 位 .NET 中对象的簿记开销为 8 个字节,而在 64 位系统上为 16 个字节。这只是开销;我们当然需要 4 个字节来存储 int。但是等等,情况变得更糟:在 64 位系统上,盒子必须与 8 字节边界对齐,所以我们需要another64 位系统上的 4 字节填充。
全部加起来:我们的int[]
在 64 位和 32 位系统上大约需要 4KB。我们的object[]
包含 1000 个整数在 32 位系统上大约需要 16KB,在 64 位系统上大约需要 32K。所以内存效率int[]
vs an object[]
对于非通用情况来说,情况要糟糕 4 到 8 倍。
但等等,情况会变得更糟。这只是尺寸。访问时间怎么样?
要访问整数数组中的整数,运行时必须:
- 验证数组是否有效
- 验证索引是否有效
- 从给定索引处的变量中获取值
要从装箱整数数组中访问整数,运行时必须:
- 验证数组是否有效
- 验证索引是否有效
- 从给定索引处的变量中获取引用
- 验证引用不为空
- 验证引用是否是装箱整数
- 从框中提取整数
步骤要多得多,因此需要更长的时间。
但等等,情况会变得更糟。
现代处理器使用芯片本身的缓存来避免返回主内存。 1000 个普通整数的数组很可能最终出现在高速缓存中,因此对快速连续的第一个、第二个、第三个等数组成员的访问都是从同一高速缓存行中提取的;这是快得离谱。但是装箱整数可能遍布整个堆,这会增加缓存未命中率,从而进一步大大减慢访问速度。
希望这足以澄清您对拳击处罚的理解。
非盒装类型怎么样?字符串数组列表和字符串数组列表之间有显着差异吗?List<string>
?
这里的惩罚要小得多,因为object[]
and a string[]
具有相似的性能特征和内存布局。在这种情况下,唯一的额外惩罚是(1)直到运行时才发现错误,(2)使代码更难以阅读和编辑,以及(3)运行时类型检查的轻微惩罚。