改进 RAM 使用行为以避免延迟

Improve RAM usage behaviour to avoid lags

我们有一个问题,似乎是由内存的不断分配和释放引起的:

我们这里有一个相当复杂的系统,其中一个 USB 设备正在测量任意点并将测量数据以每秒 50k 个样本的速率发送到 PC。然后这些样本在软件中收集为每个点的 MeasurementTasks,然后进行处理,由于计算的要求,这会导致需要更多的内存。
简化后的每个 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。完成这些计算后,我们释放所有 SampleComplexSample 实例,GC 很快就会清理它们,但这会导致内存使用量保持不变 "up and down"。
这是它现在的样子,每个 MeasurementTask 包含 ~300k 个样本:

现在我们有时会遇到硬件设备中的样本缓冲区溢出的问题,因为它只能存储约 5000 个样本(约 100 毫秒),而且似乎应用程序读取设备的速度并不总是足够快(我们使用使用 LibUSB/LibUSBDotNet 批量传输)。我们通过以下事实将此问题追溯到 "memory up and down":

目前(修复上述错误后)我们在每个点的样本数和溢出之间有直接的相关性。更多 samples/point = 更高的内存增量 = 更多的溢出。

现在进入实际问题: 是否可以(轻松)改善此行为?
也许有办法告诉 GC/runtime 不要释放内存,所以不需要重新分配?

我们还想到了 "re-using" LinkedList<Sample>ComplexSample[] 的替代方法:保留一个这样的 lists/arrays 池,而不是释放它们,将它们放回池和 "change" 这些实例而不是创建新实例,但我们不确定这是一个好主意,因为它增加了整个系统的复杂性...
但我们愿意接受其他建议!


更新:
我现在通过以下改进优化了代码库,并进行了各种测试 运行s:

无论如何,我的机器上的问题现在似乎已经消失了(接下来将进行长期测试和低规格硬件测试),但我首先 运行 对两种类型进行了测试 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)",我将开始 运行 长期(= 几个小时)和低规格硬件测试...

SampleComplexSample 更改为 struct

Can this behaviour be improved (easily)?

是的(取决于你需要改进多少)。

我要做的第一件事是将 SampleComplexSample 更改为值类型。这将降低 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;

这是因为Sampledouble需要8字节对齐,所以写的Sample是24字节,只用了18字节。

这对 LinkedList<> 来说不是一个好主意,因为 LL 有很大的开销 每个项目