是否对数组(结构类型)进行了优化以避免不必要地复制结构值?

时间:2016-03-19 22:18:10

标签: c# arrays performance linq

出于内存性能的原因,我有一个结构数组,因为项目数量很大,而且项目会被定期抛出,因此会破坏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;
}

所以,让我们说我们有一系列这些值,我想向制造商ID提取自定义slu a的字典。在我将其更改为结构之前,它是一个类,因此原始代码是使用简单的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分析器来识别正在使用多少内存以及在哪里,我发现Phalanger PHP编译器代码中存在两个内存泄漏。这是我们的进程使用的内存量持续增长的原因之一,其中一个内存泄漏真的很讨厌,实际上是由Microsoft异步代码引起的(可能值得博客或堆栈溢出问题/答案,以帮助其他人避免它)。

无论如何,一旦我发现内存泄漏并修复它们,我就推送了代码,而没有任何内存优化来从类转换为结构,奇怪的是这实际上导致了GC更多地崩溃。根据性能计数器,我看到GC将使用高达27%的CPU的时间段。由于内存泄漏,很可能这些大块以前没有得到GC,所以它们只是挂了。修复代码后,GC开始表现得比以前更差。

最后,我们完成了使用此问题中的反馈将这些类转换为结构的代码,现在我们在峰值时的总内存使用量大约是它的50%,当服务器上的负载进入时,它会迅速下降离开,更重要的是,我们看到只有0.05%的CPU用于GC,即便如此。因此,如果有人想知道这些变化是否会对现实世界产生影响,他们真的可以,特别是如果你的对象通常会闲置一段时间,那么就会卡在第二代堆中然后需要被抛出并收集垃圾

5 个答案:

答案 0 :(得分:8)

  

对这种代码进行反编译以找出代码生成器实际会为此做些什么的最佳方法是什么?

无需反编译代码。所有LINQ to Objects方法实现都可以在Reference Source看到。

关于你的具体问题。使用LINQ(以及一般基于structIEnumerable<T>的方法)时,您可以进行大量Func<T, ..>复制操作。

例如,IEnumerator<T>的当前元素是通过如下定义的属性Current访问的

T Current { get; }

所以访问至少涉及一个副本。但是枚举器实现通常在MoveNext方法期间将当前元素存储到字段中,所以我会说你可以安全地计算2个复制操作。

当然,每个Func<T, ...>都会导致另一个副本,因为T是输入参数。

因此,在这种情况下,您应该避免使用LINQ。

或者,您可以使用通过数组和索引模拟引用的旧学校技术。所以不要这样:

var dict = values
    .Where(p => !string.IsNullOrEmpty(p.CustomSlug))
    .ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);

您可以通过以下方式避免struct复制:

var dict = Enumerable.Range(0, values.Length)
    .Where(i => !string.IsNullOrEmpty(values[i].CustomSlug))
    .ToDictionary(i => values[i].CustomSlug, i => values[i].ManufacturerID);

更新:由于似乎对该主题感兴趣,我将为您提供最后一种技术的变体,它可以让您的生活更轻松,同时避免过度{{1}复制。

假设您的struct是一个类,并且您使用了许多LINQ查询,例如示例中的查询。然后你切换到ManufacturerValue

您还可以像这样创建包装器struct和帮助器扩展方法

struct

它的额外(一次)努力,但具有以下好处:

(1)它是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; } public struct ManufacturerValueRef { public readonly ManufacturerValue[] Source; public readonly int Index; public ManufacturerValueRef(ManufacturerValue[] source, int index) { Source = source; Index = index; } public int ManufacturerID => Source[Index].ManufacturerID; public string Name => Source[Index].Name; public string CustomSlug => Source[Index].CustomSlug; public string Title => Source[Index].Title; public string Description => Source[Index].Description; public string Image => Source[Index].Image; public string SearchFilters => Source[Index].SearchFilters; public int TopZoneProduction => Source[Index].TopZoneProduction; public int TopZoneTesting => Source[Index].TopZoneTesting; public int ActiveProducts => Source[Index].ActiveProducts; } public static partial class Utils { public static IEnumerable<ManufacturerValueRef> AsRef(this ManufacturerValue[] values) { for (int i = 0; i < values.Length; i++) yield return new ManufacturerValueRef(values, i); } } ,但具有固定大小,因此与正常参考(一个额外的struct)相比,复制开销可以忽略不计。
(2)您可以扩展实际数据int大小而不用担心 (3)您需要对LINQ查询执行的操作是添加struct

样品:

.AsRef()

答案 1 :(得分:4)

结构是[按价值传递] [1] - 所以我非常肯定只是为你的ToDictionary使用代表的行为将导致两个副本,无论发生了什么。

换句话说,请考虑

.ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);

等同于:

var key = GetKey(values[i]);
var value = GetValue(values[i]);

.ToDictionary(key, value);

显然会创建两个结构副本以传递给GetKeyGetValue

答案 2 :(得分:3)

如果您需要稍微放松一下垃圾收集器,您可能需要在app.config文件中使用gcServer选项:

<configuration>
    <runtime>
        <gcServer enabled="true" />
    </runtime>
</configuration>

要查看基于LINQ代码生成的IL类型,LinqPad是一个很棒的工具。

不幸的是,我对使用LINQ反对结构的枚举没有任何线索。我通常使用结构来保留一些值类型。

也许放松GC可以帮助您规避性能问题,给课程另外一次机会?我还有一个应用程序可以进行大量的对象创建和处理,其中表演由GC Frenzy设置。使用GCServer =“true”解决了它,以换取使用私有内存的轻微增加。

答案 3 :(得分:0)

箭头:

p => !string.IsNullOrEmpty(p.CustomSlug)
p => p.CustomSlug
p => p.ManufacturerID
每个

都被编译成一个实际的方法,其中p是方法的值参数。然后,这些方法以Func委托实例的形式传递给Linq。由于它们是值参数,因此您的结构将按值传递。

也许你可以使用:

ManufacturerValue[] values = GetManufacturerValues();
var dict = Enumerate.Range(0, values.Length)
  .Where(i => !string.IsNullOrEmpty(values[i].CustomSlug))
  .ToDictionary(i => values[i].CustomSlug, i => values[i].ManufacturerID);

这只捕获每个lambda箭头(闭包)中的数组引用。

编辑:我没有看到Ivan Stoev的回答已经有了这个建议。反而回答他的回答。

答案 4 :(得分:0)

我已经对大小不一的1000万个结构对Linq的Where()的性能进行了基准测试。

在所有情况下结构都更快。

代码:https://github.com/Erikvv/linq-large-struct-benchmark