C#中高效的队列清理
Efficient queue clearing in C#
我目前正在处理一个队列,其中有几千个条目。为了节省 RAM 使用量,我目前正在使用 Microsoft 内置到队列数据类型中的 TrimExcess() 方法。如documentation中所述
,此方法在处理大型列表时效率低下,每次调用都会导致大量时间损失。
是否有更有效的方法从队列中删除项目,实际上也会在 RAM 中删除它们?
编辑:为了说明队列中还有元素,我只想删除已经从 RAM 中出队的元素
你的问题的答案是“不用担心,你很可能不应该那样做。”这个答案是对评论的阐述@madreflection 和我自己。
Queue<T>
class 和其他集合一样 classes 使用数组来保存它收集的项目。与其他集合 classes 一样,如果数组用完槽,队列 class 会创建一个新的、更大的数组并从旧数组复制数据。
我没有查看 Queue<T>
源代码,但是使用调试器,我可以看到这个数组是 class 的 _array
成员。它以 0
长度的数组开头。当您将一个项目排入队列时,它会被长度为 4 的数组替换。之后,只要需要更多,数组的大小就会加倍 space.
您说您的队列 “其中有几千个条目”。我将在此分析中使用 2000 作为粗略猜测。
随着您将越来越多的净条目加入队列,数组加倍会发生几次:
At First
After 5
After 9
After 17
After 33
4 Entries
Double to 8
Double to 16
Double to 32
Double to 64
它将继续这样做,直到它加倍(并复制数组内容)10 次 - 使其达到 2048。此时,您将分配 10 个数组,其中九个是垃圾,并完成大约 3000 个排队元素副本。
现在想想。我猜你正在排队引用类型对象。引用类型对象由对象引用(实际上是指针)表示。如果队列中有 2000 个实例,在 32 位机器上将代表 8kb(加上队列成员的一些 one-time 开销 class)。在 64 位机器上,它是 16kb。这对现代计算机来说不算什么。 .NET 垃圾收集器有两种管理内存的策略,一种是普通策略,一种是针对大对象。边界为85kb;你的队列永远不会是大对象
如果要对大值类型进行排队,则需要更多内存(因为值类型对象将被复制到构成队列条目的数组元素中)。在队列变成 大对象.
之前,您需要使用非常大的值类型对象
将发生的另一件事是,随着队列大小的增长,它将进入垃圾收集器的 Gen2 内存区域。 Gen2 收集是昂贵的,但是一旦一个对象在 Gen2 中变得稳定,它就根本不会打扰垃圾收集器。
但是,想一想如果您将队列大小减少到 100 个条目并调用 TrimExcess
会发生什么。那时,将创建另一个新数组(这次,小得多)并且队列中的条目将被复制到该新队列(这就是 Remarks 部分中的注释TrimExcess
文档中提到 重新分配和复制大型 Queue<T>
的成本)。如果您的队列再次开始增长,您将一遍又一遍地启动 doubling/copying 该数组 - 产生更多垃圾并旋转轮子进行复制。
更好的策略是查看您估计的队列大小,稍微扩大它,并在构造时 pre-allocate space 所有这些条目。如果您希望有 2000 个条目,请在构造函数中为 2500 分配 space:
var myQueue = new Queue<SomeType>(2500);
现在,你做一次分配,应该没有重新分配或数组复制,你的内存会很快迁移到Gen2,但永远不会被GC触及。
我目前正在处理一个队列,其中有几千个条目。为了节省 RAM 使用量,我目前正在使用 Microsoft 内置到队列数据类型中的 TrimExcess() 方法。如documentation中所述 ,此方法在处理大型列表时效率低下,每次调用都会导致大量时间损失。
是否有更有效的方法从队列中删除项目,实际上也会在 RAM 中删除它们?
编辑:为了说明队列中还有元素,我只想删除已经从 RAM 中出队的元素
你的问题的答案是“不用担心,你很可能不应该那样做。”这个答案是对评论的阐述@madreflection 和我自己。
Queue<T>
class 和其他集合一样 classes 使用数组来保存它收集的项目。与其他集合 classes 一样,如果数组用完槽,队列 class 会创建一个新的、更大的数组并从旧数组复制数据。
我没有查看 Queue<T>
源代码,但是使用调试器,我可以看到这个数组是 class 的 _array
成员。它以 0
长度的数组开头。当您将一个项目排入队列时,它会被长度为 4 的数组替换。之后,只要需要更多,数组的大小就会加倍 space.
您说您的队列 “其中有几千个条目”。我将在此分析中使用 2000 作为粗略猜测。
随着您将越来越多的净条目加入队列,数组加倍会发生几次:
At First | After 5 | After 9 | After 17 | After 33 |
---|---|---|---|---|
4 Entries | Double to 8 | Double to 16 | Double to 32 | Double to 64 |
它将继续这样做,直到它加倍(并复制数组内容)10 次 - 使其达到 2048。此时,您将分配 10 个数组,其中九个是垃圾,并完成大约 3000 个排队元素副本。
现在想想。我猜你正在排队引用类型对象。引用类型对象由对象引用(实际上是指针)表示。如果队列中有 2000 个实例,在 32 位机器上将代表 8kb(加上队列成员的一些 one-time 开销 class)。在 64 位机器上,它是 16kb。这对现代计算机来说不算什么。 .NET 垃圾收集器有两种管理内存的策略,一种是普通策略,一种是针对大对象。边界为85kb;你的队列永远不会是大对象
如果要对大值类型进行排队,则需要更多内存(因为值类型对象将被复制到构成队列条目的数组元素中)。在队列变成 大对象.
之前,您需要使用非常大的值类型对象将发生的另一件事是,随着队列大小的增长,它将进入垃圾收集器的 Gen2 内存区域。 Gen2 收集是昂贵的,但是一旦一个对象在 Gen2 中变得稳定,它就根本不会打扰垃圾收集器。
但是,想一想如果您将队列大小减少到 100 个条目并调用 TrimExcess
会发生什么。那时,将创建另一个新数组(这次,小得多)并且队列中的条目将被复制到该新队列(这就是 Remarks 部分中的注释TrimExcess
文档中提到 重新分配和复制大型 Queue<T>
的成本)。如果您的队列再次开始增长,您将一遍又一遍地启动 doubling/copying 该数组 - 产生更多垃圾并旋转轮子进行复制。
更好的策略是查看您估计的队列大小,稍微扩大它,并在构造时 pre-allocate space 所有这些条目。如果您希望有 2000 个条目,请在构造函数中为 2500 分配 space:
var myQueue = new Queue<SomeType>(2500);
现在,你做一次分配,应该没有重新分配或数组复制,你的内存会很快迁移到Gen2,但永远不会被GC触及。