改善RAM使用行为以避免滞后

时间:2015-05-27 11:54:42

标签: c# .net performance memory real-time

我们遇到的问题似乎是由内存的不断分配和释放引起的:

我们这里有一个相当复杂的系统,USB设备正在测量任意点,并以每秒50k样本的速率将测量数据发送到PC。然后在软件中为每个点收集这些样本MeasurementTask,然后进行处理,由于计算的要求,这会导致更多所需的内存。
每个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很快会清除它们,但这会产生一个常数&#34; up&down&#34;内存使用情况。
这就是每个MeasurementTask包含~300k样本的时刻: RAM usage

现在我们有时会遇到硬件设备中的样本缓冲区溢出的问题,因为它只能存储约5000个样本(~100ms),而且应用程序似乎并不总是能够足够快地读取设备(我们使用BULK传输)与LibUSB / LibUSBDotNet)。我们将这个问题追溯到这个&#34;内存上下来&#34;通过以下事实:

  • 来自USB设备的读取发生在自己的线程中,该线程在ThreadPriority.Highest运行,因此计算不应该干扰
  • 我的8核CPU上的CPU使用率介于1-5%之间=&gt; &lt;一个核心的50%
  • 如果我们有(更多)更快MeasurementTask s,每个只有几个hundret样本,内存只会上下很少,缓冲区永远不会溢出(但实例/秒的数量是相同的,因为设备仍然发送50k样本/秒)
  • 之前我们遇到过一个错误,它在计算后没有释放SampleComplexSample个实例,因此内存仅以~2-3 MB / s的速度上升,缓冲区全部超过时间

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

现在回答实际问题: 可以(轻松)改善这种行为吗?
也许有办法告诉GC /运行时不释放内存,所以不需要重新分配?

我们还想到了另一种方法,即重新使用&#34; LinkedList<Sample>ComplexSample[]:保留一个这样的列表/数组池,而不是释放它们将它们放回池中,然后更改&#34;这些实例而不是创建新实例,但我们不确定这是一个好主意,因为它增加了整个系统的复杂性...
但我们对其他建议持开放态度!

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

  • Sample转换为struct
  • 摆脱了LinkedList<Sample>并用straigt数组替换了它们(我实际上还有另外一个我也删除了)
  • 我在分析和优化过程中发现的一些小优化
  • (可选 - 见下文)将ComplexSample转换为结构

在任何情况下,现在我的机器上的问题似乎都消失了(长期测试和低规格硬件的测试将会随之而来),但我首先运行两种类型的测试struct并得到了以下内存使用情况图:
structs
在那里,它仍然定期达到约300 MB(但不再出现溢出错误),但由于这对我来说似乎仍然很奇怪,我做了一些额外的测试:

旁注:每个ComplexSample的每个值在计算过程中至少更改一次。

1)在处理任务后添加GC.Collect并且不再引用样本:
Struct with GC.Collect
现在它在140 MB到150 MB之间交替(没有明显的性能命中)。

2)ComplexSample作为一个班级(没有GC.Collect):
class
使用课程更加稳定&#34;在~140-200 MB。

3)ComplexSample作为班级GC.Collect
class with GC.Collect
现在它正在上升和下降&#34;有点在135-150 MB之间。

当前解决方案:
由于我们不确定这是手动调用GC.Collect的有效案例,我们正在使用&#34;解决方案2)&#34;现在,我将开始运行长期(=几个小时)和低规格的硬件测试......

3 个答案:

答案 0 :(得分:3)

  

可以(轻松)改善这种行为吗?

是(取决于你需要多少才能改善它)。

我要做的第一件事就是将SampleComplexSample更改为值类型。这将降低GC处理的图形的复杂性,因为仍然收集数组和链表,它们直接包含这些值而不是对它们的引用,这简化了GC的其余部分。

然后我会测量此时的表现。使用相对较大结构的影响是复杂的。值类型应该小于16个字节的指导原则来自于使用引用类型的性能优势倾向于压倒使用值类型的性能优势的那一点,但该指南只是一个指导因为“倾向于压倒性的“不一样”会在你的申请中淹没“。

之后如果它没有改进的东西,或者没有足够的改进,我会考虑使用一个对象池;是对于那些较小的对象,只是较大的对象,还是两者。这肯定会增加应用程序的复杂性,但如果它对时间要求很高,那么它可能会有所帮助。 (例如,参见How do these people avoid creating any garbage?讨论在时间紧迫的情况下避免正常GC的问题。)

如果你知道你需要一个给定类型的固定最大值,这不是太难;创建并填充它们的数组,并在返回之前将它们从该数组中分离出来,因为它们不再使用。它仍然很难,因为你不再有GC是自动的,必须通过将它们放回池中来手动“删除”它们。

如果你没有这方面的知识,那就会变得更难,但仍有可能。

如果避免GC非常重要,请注意隐藏的物体。添加到大多数集合类型可以例如导致它们向上移动到更大的内部存储,并且留下要收集的早期存储。也许这很好,因为你仍然减少了GC的使用,不再导致你的问题,但可能不会。

答案 1 :(得分:2)

我很少见过.NET中使用的LinkedList<> ...您是否尝试使用List<>?考虑LinkedList<>的基本“元素”是LinkedListNode<>,它是 ...因此对于每个Sample,还有一个额外的开销一个对象。

请注意,如果您想使用“大”值类型(正如其他人所建议的那样),List<>可能会再次变慢(因为List<>增长“生成新的内部数组加倍当前大小并从旧复制到新文件,因此元素越大,List<>在自身加倍时必须复制的内存越多。

如果您转到List<>,可以尝试将Sample拆分为

List<ushort> CommandIndex;
List<Sample> ValueChannels;

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

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

答案 2 :(得分:1)

SampleComplexSample更改为struct