改进 RAM 使用行为以避免延迟
Improve RAM usage behaviour to avoid lags
我们有一个问题,似乎是由内存的不断分配和释放引起的:
我们这里有一个相当复杂的系统,其中一个 USB 设备正在测量任意点并将测量数据以每秒 50k 个样本的速率发送到 PC。然后这些样本在软件中收集为每个点的 MeasurementTask
s,然后进行处理,由于计算的要求,这会导致需要更多的内存。
简化后的每个 MeasurementTask
如下所示:
public class MeasurementTask
{
public LinkedList<Sample> Samples { get; set; }
public ComplexSample[] ComplexSamples { get; set; }
public Complex Result { get; set; }
}
其中 Sample
看起来像:
public class Sample
{
public ushort CommandIndex;
public double ValueChannel1;
public double ValueChannel2;
}
和ComplexSample
喜欢:
public class ComplexSample
{
public double Channel1Real;
public double Channel1Imag;
public double Channel2Real;
public double Channel2Imag;
}
在计算过程中,首先将Samples
计算成一个ComplexSample
,然后进一步处理,直到得到我们的Complex Result
。完成这些计算后,我们释放所有 Sample
和 ComplexSample
实例,GC 很快就会清理它们,但这会导致内存使用量保持不变 "up and down"。
这是它现在的样子,每个 MeasurementTask
包含 ~300k 个样本:
现在我们有时会遇到硬件设备中的样本缓冲区溢出的问题,因为它只能存储约 5000 个样本(约 100 毫秒),而且似乎应用程序读取设备的速度并不总是足够快(我们使用使用 LibUSB/LibUSBDotNet 批量传输)。我们通过以下事实将此问题追溯到 "memory up and down":
- USB 设备的读取发生在它自己的线程中,运行位于
ThreadPriority.Highest
,因此计算不应干扰
- CPU 在我的 8 核上使用率在 1-5% 之间 CPU => <1 核的 50%
- 如果我们有(多)快的
MeasurementTask
s,每个样本只有几个 hundret 样本,内存只会上下变化很少,缓冲区永远不会溢出(但是 instances/second是一样的,因为设备仍然发送 50k samples/second)
- 我们之前有一个错误,在计算之后没有释放
Sample
和 ComplexSample
实例,所以内存只在 ~2-3 MB/s 和缓冲区一直溢出
目前(修复上述错误后)我们在每个点的样本数和溢出之间有直接的相关性。更多 samples/point = 更高的内存增量 = 更多的溢出。
现在进入实际问题:
是否可以(轻松)改善此行为?
也许有办法告诉 GC/runtime 不要释放内存,所以不需要重新分配?
我们还想到了 "re-using" LinkedList<Sample>
和 ComplexSample[]
的替代方法:保留一个这样的 lists/arrays 池,而不是释放它们,将它们放回池和 "change" 这些实例而不是创建新实例,但我们不确定这是一个好主意,因为它增加了整个系统的复杂性...
但我们愿意接受其他建议!
更新:
我现在通过以下改进优化了代码库,并进行了各种测试 运行s:
- 已将
Sample
转换为 struct
- 摆脱了
LinkedList<Sample>
并用直接数组替换它们(实际上我在其他地方有另一个我也删除了)
- 我在分析和优化过程中发现的几个小优化
- (可选 - 见下文)将
ComplexSample
转换为结构
无论如何,我的机器上的问题现在似乎已经消失了(接下来将进行长期测试和低规格硬件测试),但我首先 运行 对两种类型进行了测试 struct
并得到以下内存使用图:
它仍然定期上升到 ~300 MB(但不再有溢出错误),但由于这对我来说仍然很奇怪,所以我做了一些额外的测试:
旁注:每个 ComplexSample
的每个值在计算过程中至少更改一次。
1) 在任务处理完成后添加一个GC.Collect
,样本不再被引用:
现在它在 140 MB 和 150 MB 之间交替(没有明显的性能下降)。
2) ComplexSample
作为 class(没有 GC.Collect
):
使用 class 在 ~140-200 MB 时 "stable" 多得多。
3) ComplexSample
作为 class 和 GC.Collect
:
现在它 "up and down" 在 135-150 MB 的范围内。
当前解法:
由于我们不确定这是手动调用 GC.Collect
的有效案例,我们现在使用 "solution 2)",我将开始 运行 长期(= 几个小时)和低规格硬件测试...
将 Sample
和 ComplexSample
更改为 struct
。
Can this behaviour be improved (easily)?
是的(取决于你需要改进多少)。
我要做的第一件事是将 Sample
和 ComplexSample
更改为值类型。这将降低 GC 处理的图形的复杂性,因为当数组和链表仍然被收集时,它们直接包含这些值而不是对它们的引用,这简化了 GC 的其余部分。
然后我会在此时衡量性能。使用相对较大的结构的影响是复杂的。值类型应小于 16 字节的准则来自于使用引用类型的性能优势往往压倒使用值类型的性能优势的这一点,但该准则只是一个准则,因为 "tend to overwhelm" 与 "will overwhelm in your application".
不同
之后如果没有改进,或者改进不够,我会考虑使用对象池;是否针对那些较小的对象,仅针对较大的对象,或两者兼而有之。这肯定会增加应用程序的复杂性,但如果时间紧迫,那么它可能会有所帮助。 (例如,参见 How do these people avoid creating any garbage?,其中讨论了在时间紧迫的情况下避免正常 GC)。
如果您知道您将需要给定类型的固定最大值,这并不难;创建并填充它们的数组,并在返回它们之前从该数组中分发它们,因为它们不再被使用。它仍然很难,因为您不再让 GC 是自动的并且必须通过将对象放回池中来手动 "delete" 对象。
如果你没有这些知识,它会变得更难,但仍然是可能的。
如果避免 GC 真的很重要,请注意隐藏的对象。例如,添加到大多数集合类型可能会导致它们向上移动到更大的内部存储,并离开较早的存储以进行收集。也许这很好,因为您仍然充分减少了 GC 使用,以至于它不再导致您遇到的问题,但也许不会。
我很少看到 .NET 中使用 LinkedList<>
...您尝试过使用 List<>
吗?考虑到 LinkedList<>
的基本 "element" 是 LinkedListNode<>
,即 class... 所以对于每个 Sample
有一个对象的整体额外开销。
请注意,如果您想使用 "big" 值类型(如其他人所建议的),List<>
可能会再次变慢(因为 List<>
通过“生成新的- 当前大小加倍的内部数组,并从旧的复制到新的),因此元素越大,List<>
在自身加倍时必须复制的内存越多。
如果您转到 List<>
,您可以尝试将 Sample
拆分为
List<ushort> CommandIndex;
List<Sample> ValueChannels;
这是因为Sample
的double
需要8字节对齐,所以写的Sample
是24字节,只用了18字节。
这对 LinkedList<>
来说不是一个好主意,因为 LL 有很大的开销 每个项目 。
我们有一个问题,似乎是由内存的不断分配和释放引起的:
我们这里有一个相当复杂的系统,其中一个 USB 设备正在测量任意点并将测量数据以每秒 50k 个样本的速率发送到 PC。然后这些样本在软件中收集为每个点的 MeasurementTask
s,然后进行处理,由于计算的要求,这会导致需要更多的内存。
简化后的每个 MeasurementTask
如下所示:
public class MeasurementTask
{
public LinkedList<Sample> Samples { get; set; }
public ComplexSample[] ComplexSamples { get; set; }
public Complex Result { get; set; }
}
其中 Sample
看起来像:
public class Sample
{
public ushort CommandIndex;
public double ValueChannel1;
public double ValueChannel2;
}
和ComplexSample
喜欢:
public class ComplexSample
{
public double Channel1Real;
public double Channel1Imag;
public double Channel2Real;
public double Channel2Imag;
}
在计算过程中,首先将Samples
计算成一个ComplexSample
,然后进一步处理,直到得到我们的Complex Result
。完成这些计算后,我们释放所有 Sample
和 ComplexSample
实例,GC 很快就会清理它们,但这会导致内存使用量保持不变 "up and down"。
这是它现在的样子,每个 MeasurementTask
包含 ~300k 个样本:
现在我们有时会遇到硬件设备中的样本缓冲区溢出的问题,因为它只能存储约 5000 个样本(约 100 毫秒),而且似乎应用程序读取设备的速度并不总是足够快(我们使用使用 LibUSB/LibUSBDotNet 批量传输)。我们通过以下事实将此问题追溯到 "memory up and down":
- USB 设备的读取发生在它自己的线程中,运行位于
ThreadPriority.Highest
,因此计算不应干扰 - CPU 在我的 8 核上使用率在 1-5% 之间 CPU => <1 核的 50%
- 如果我们有(多)快的
MeasurementTask
s,每个样本只有几个 hundret 样本,内存只会上下变化很少,缓冲区永远不会溢出(但是 instances/second是一样的,因为设备仍然发送 50k samples/second) - 我们之前有一个错误,在计算之后没有释放
Sample
和ComplexSample
实例,所以内存只在 ~2-3 MB/s 和缓冲区一直溢出
目前(修复上述错误后)我们在每个点的样本数和溢出之间有直接的相关性。更多 samples/point = 更高的内存增量 = 更多的溢出。
现在进入实际问题:
是否可以(轻松)改善此行为?
也许有办法告诉 GC/runtime 不要释放内存,所以不需要重新分配?
我们还想到了 "re-using" LinkedList<Sample>
和 ComplexSample[]
的替代方法:保留一个这样的 lists/arrays 池,而不是释放它们,将它们放回池和 "change" 这些实例而不是创建新实例,但我们不确定这是一个好主意,因为它增加了整个系统的复杂性...
但我们愿意接受其他建议!
更新:
我现在通过以下改进优化了代码库,并进行了各种测试 运行s:
- 已将
Sample
转换为struct
- 摆脱了
LinkedList<Sample>
并用直接数组替换它们(实际上我在其他地方有另一个我也删除了) - 我在分析和优化过程中发现的几个小优化
- (可选 - 见下文)将
ComplexSample
转换为结构
无论如何,我的机器上的问题现在似乎已经消失了(接下来将进行长期测试和低规格硬件测试),但我首先 运行 对两种类型进行了测试 struct
并得到以下内存使用图:
它仍然定期上升到 ~300 MB(但不再有溢出错误),但由于这对我来说仍然很奇怪,所以我做了一些额外的测试:
旁注:每个 ComplexSample
的每个值在计算过程中至少更改一次。
1) 在任务处理完成后添加一个GC.Collect
,样本不再被引用:
现在它在 140 MB 和 150 MB 之间交替(没有明显的性能下降)。
2) ComplexSample
作为 class(没有 GC.Collect
):
使用 class 在 ~140-200 MB 时 "stable" 多得多。
3) ComplexSample
作为 class 和 GC.Collect
:
现在它 "up and down" 在 135-150 MB 的范围内。
当前解法:
由于我们不确定这是手动调用 GC.Collect
的有效案例,我们现在使用 "solution 2)",我将开始 运行 长期(= 几个小时)和低规格硬件测试...
将 Sample
和 ComplexSample
更改为 struct
。
Can this behaviour be improved (easily)?
是的(取决于你需要改进多少)。
我要做的第一件事是将 Sample
和 ComplexSample
更改为值类型。这将降低 GC 处理的图形的复杂性,因为当数组和链表仍然被收集时,它们直接包含这些值而不是对它们的引用,这简化了 GC 的其余部分。
然后我会在此时衡量性能。使用相对较大的结构的影响是复杂的。值类型应小于 16 字节的准则来自于使用引用类型的性能优势往往压倒使用值类型的性能优势的这一点,但该准则只是一个准则,因为 "tend to overwhelm" 与 "will overwhelm in your application".
不同之后如果没有改进,或者改进不够,我会考虑使用对象池;是否针对那些较小的对象,仅针对较大的对象,或两者兼而有之。这肯定会增加应用程序的复杂性,但如果时间紧迫,那么它可能会有所帮助。 (例如,参见 How do these people avoid creating any garbage?,其中讨论了在时间紧迫的情况下避免正常 GC)。
如果您知道您将需要给定类型的固定最大值,这并不难;创建并填充它们的数组,并在返回它们之前从该数组中分发它们,因为它们不再被使用。它仍然很难,因为您不再让 GC 是自动的并且必须通过将对象放回池中来手动 "delete" 对象。
如果你没有这些知识,它会变得更难,但仍然是可能的。
如果避免 GC 真的很重要,请注意隐藏的对象。例如,添加到大多数集合类型可能会导致它们向上移动到更大的内部存储,并离开较早的存储以进行收集。也许这很好,因为您仍然充分减少了 GC 使用,以至于它不再导致您遇到的问题,但也许不会。
我很少看到 .NET 中使用 LinkedList<>
...您尝试过使用 List<>
吗?考虑到 LinkedList<>
的基本 "element" 是 LinkedListNode<>
,即 class... 所以对于每个 Sample
有一个对象的整体额外开销。
请注意,如果您想使用 "big" 值类型(如其他人所建议的),List<>
可能会再次变慢(因为 List<>
通过“生成新的- 当前大小加倍的内部数组,并从旧的复制到新的),因此元素越大,List<>
在自身加倍时必须复制的内存越多。
如果您转到 List<>
,您可以尝试将 Sample
拆分为
List<ushort> CommandIndex;
List<Sample> ValueChannels;
这是因为Sample
的double
需要8字节对齐,所以写的Sample
是24字节,只用了18字节。
这对 LinkedList<>
来说不是一个好主意,因为 LL 有很大的开销 每个项目 。