数组(结构类型)的 Where 是不是已优化以避免不必要地复制结构值?
Posted
技术标签:
【中文标题】数组(结构类型)的 Where 是不是已优化以避免不必要地复制结构值?【英文标题】:Is Where on an Array (of a struct type) optimized to avoid needless copying of struct values?数组(结构类型)的 Where 是否已优化以避免不必要地复制结构值? 【发布时间】: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 倍的,并且有更多的值浮动,所以我想先在这个上做对。并且发布该问题会为问题添加大量代码:)
【参考方案1】:
反编译此类代码以了解代码生成器实际上会为此做什么的最佳方法是什么?
无需反编译代码。所有 LINQ to Objects 方法的实现都可以在Reference Source 看到。
关于您的具体问题。使用 LINQ(以及通常基于 IEnumerable<T>
和 Func<T, ..>
的方法)时,您可以期待大量的 struct
复制操作。
例如,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);
更新:由于似乎对这个主题很感兴趣,我将为您提供最后一种技术的变体,它可以让您的生活更轻松,同时避免过多的struct
复制。
假设您的 ManufacturerValue
是一个类,并且您使用了很多 LINQ 查询,例如示例中的查询。然后你切换到struct
。
您还可以像这样创建包装器struct
和辅助扩展方法
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);
这是额外的(一次性)工作,但具有以下好处:
(1) 它是一个struct
,但具有固定大小,因此与普通引用相比,复制开销可以忽略不计(一个额外的int
)。
(2) 您可以扩展实际数据struct
大小而无需担心。
(3) LINQ 查询只需添加.AsRef()
示例:
var dict = values.AsRef()
.Where(p => !string.IsNullOrEmpty(p.CustomSlug))
.ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
【讨论】:
value ref 模式非常简洁。我喜欢。我正在考虑类似的事情,这是有道理的。 ValueRef 也是结构而不是类的原因是什么?我猜它里面只有两个实际值(引用和索引),所以把它做成一个结构也很有意义,因为复制它会很便宜。 我从未见过 public int ManufacturerID => Source[Index].ManufacturerID;之前的语法。那是某种代表吸气剂的 Lambda(啊,我看到了 C# 6)?两者在编译时有什么区别?我暗中希望这可能意味着该函数将被内联,但我对此表示怀疑:) 它是一个 C#6 编译器 功能,称为 Expression bodied members,只是定义属性/方法的糖。它与public int ManufacturerID get return Source[Index].ManufacturerID;
完全一样,根本不是 lambda。而且没有魔法:)【参考方案2】:
结构是 [按值传递][1] - 所以我相当肯定,只是为您的 ToDictionary
使用委托的行为将导致两个副本,无论发生什么其他事情。
换句话说,考虑
.ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
相当于:
var key = GetKey(values[i]);
var value = GetValue(values[i]);
.ToDictionary(key, value);
这显然会创建结构的两个副本以传递给GetKey
和GetValue
。
【讨论】:
有趣。在使用 LINQ 时,这实际上会比我的幼稚示例更糟糕。我认为如果你在做大型结构的数组,答案是避免使用 LINQ。谢谢! @KendallBennett - 嗯......我会犹豫说你的幼稚和不幼稚或 LINQ 案例是否以及在性能上会产生多大的差异;在 JIT 中可以进行很多优化....无法替代测量和/或查看 JITted 代码.... 是的,但是如果结构包含大量数据,那么性能差异将是可衡量的。【参考方案3】:如果您需要稍微放松一下垃圾收集器,您可能需要在 app.config 文件中使用 gcServer 选项:
<configuration>
<runtime>
<gcServer enabled="true" />
</runtime>
</configuration>
要查看基于您的 LINQ 代码生成的 IL 类型,LinqPad 是一个很棒的工具。
不幸的是,我对使用 LINQ 对结构枚举一无所知。我通常使用结构来保留少量的值类型。
也许放宽 GC 可以帮助您规避性能问题,并给课程另一个机会?我还有一个应用程序可以进行大量的对象创建和处理,其性能被 GC Frenzy 所拖累。使用 GCServer="true" 解决了这个问题,但使用的私有内存略有增加。
【讨论】:
此代码在 Web 服务器中运行,因此它已经在使用服务器 GC 选项。我们注意到,在我们的网站运行一段时间后,我们的内存使用率上升,CPU 使用率上升(几乎翻了一番)。似乎我们经常看到 GC 中使用了 6-8% 的 CPU,而且站点运行的时间越长,情况就越糟。 "此代码在 Web 服务器中运行,因此它已经在使用服务器 GC 选项":恐怕服务器的 machine.config 中默认未启用 GCServer。出于好奇,这个条目实际上是否存在于 machine.config 或 yourapp.config 文件中?【参考方案4】:箭头:
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 的回答已经有这个建议。改为支持他的答案。
【讨论】:
【参考方案5】:我已经在 1000 万个结构与大大小小的类上对 Linq 的 Where() 的性能进行了基准测试。
结构在所有情况下都更快。
代码:https://github.com/Erikvv/linq-large-struct-benchmark
【讨论】:
以上是关于数组(结构类型)的 Where 是不是已优化以避免不必要地复制结构值?的主要内容,如果未能解决你的问题,请参考以下文章