【发布时间】:2016-07-06 14:17:06
【问题描述】:
出于内存性能的原因,我有一个结构数组,因为项目的数量很大,并且项目会定期被扔掉,因此会破坏 GC 堆。这不是我是否应该使用大型结构的问题;我已经确定 GC 垃圾处理会导致性能问题。我的问题是当我需要处理这个结构数组时,我应该避免使用 LINQ 吗?由于结构并不小,因此按值传递它是不明智的,而且我不知道 LINQ 代码生成器是否足够聪明以执行此操作。结构如下:
public struct ManufacturerValue
{
public int ManufacturerID;
public string Name;
public string CustomSlug;
public string Title;
public string Description;
public string Image;
public string SearchFilters;
public int TopZoneProduction;
public int TopZoneTesting;
public int ActiveProducts;
}
假设我们有一个包含这些值的数组,我想提取一个自定义 slug 的字典到制造商 ID。在我将其更改为结构之前,它是一个类,因此原始代码是使用简单的 LINQ 查询编写的:
ManufacturerValue[] = GetManufacturerValues();
var dict = values.Where(p => !string.IsNullOrEmpty(p.CustomSlug))
.ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
我担心的是我想了解 LINQ 将如何生成实际代码来构建这个字典。我的怀疑是,在内部,LINQ 代码最终会变成这样的幼稚实现:
var dict = new Dictionary<string, int>();
for (var i = 0; i < values.Length; i++) {
var value = values[i];
if (!string.IsNullOrEmpty(value.CustomSlug)) {
dict.Add(value.CustomSlug, value.ManufacturerID);
}
}
这会很糟糕,因为第三行将创建结构的本地副本,这会很慢,因为结构很大并且会破坏内存总线。除了 ID 和自定义 slug 之外,我们也不需要任何东西,因此它会在每次迭代中复制很多无用的信息。如果我自己高效地编写代码,我会这样写:
var dict = new Dictionary<string, int>();
for (var i = 0; i < values.Length; i++) {
if (!string.IsNullOrEmpty(values[i].CustomSlug)) {
dict.Add(values[i].CustomSlug, values[i].ManufacturerID);
}
}
那么有谁知道代码生成器是否足够聪明,可以像第二个示例那样使用简单的数组索引,当生成器代码在结构数组上运行时,还是会实现更幼稚但更慢的第一个实现?
反编译此类代码以了解代码生成器实际上会为此做什么的最佳方法是什么?
更新
这些更改现已投入生产。事实证明,在重写代码和使用 Dot Memory profiler 来确定正在使用多少内存以及在哪里使用的过程中,我在 Phalanger PHP 编译器代码中发现了两个内存泄漏。这是我们的进程使用的内存量不断增长的原因之一,其中一个内存泄漏非常严重,实际上是由 Microsoft Async 代码引起的(可能值得写一篇博客或堆栈溢出问题/答案,以帮助其他人避免它)。
无论如何,一旦我发现了内存泄漏并修复了它们,我就在没有任何内存优化将类转换为结构的情况下推送了该代码,奇怪的是,这实际上导致 GC 更加颠簸。根据性能计数器,我看到 GC 将使用高达 27% 的 CPU 的时间段。由于内存泄漏,这些大块很可能以前没有得到 GC,所以它们只是闲逛。修复代码后,GC 的行为开始变得比以前更差。
最后,我们使用这个问题中的反馈完成了将这些类转换为结构的代码,现在我们在峰值时的总内存使用量约为原来的 50%,当服务器上的负载下降时,它会迅速下降更重要的是,我们看到只有 0.05% 的 CPU 用于 GC,即使这样。因此,如果有人想知道这些更改是否会对现实世界产生影响,他们确实可以,特别是如果您的对象通常会停留一段时间,因此会卡在第二代堆中,然后需要被扔掉并收集垃圾.
【问题讨论】:
-
看看WhereArrayIterator。此类的
MoveNext方法包含这一行:TSource item = source[index];。我认为这意味着这样的分配将导致系统复制您拥有的结构的内容。 -
您可以使用代理模式在重量级结构前面放置轻量级代理类。
-
@Amit 如果 C# 语言能够创建指向结构的引用指针,就像我们在 C++ 中所做的那样,这一切都会简单得多。但话又说回来,这可能会对 GC 造成严重破坏。权衡取舍...
-
如果您不清楚,就性能而言,LINQ 查询永远不会像使用原始循环的查询一样好。除了非常特殊的情况,LINQ to objects 的实现都是基于枚举器,它有自己的开销。
-
@user2864740 不,它会完全在堆栈上。我不担心由此引起的任何 GC 压力,而只是增加了所有数据复制的内存总线开销。这是我要重写的第一个结构。我还有一个大小大约是 4 倍的,并且有更多的值浮动,所以我想先在这个上做对。并且发布该问题会为问题添加大量代码:)
标签: c# arrays performance linq