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

Ken*_*ett 15 c# linq arrays performance

出于内存性能的原因,我有一个结构数组,因为项目数量很大,而且项目经常被抛出,因此会破坏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;
}
Run Code Online (Sandbox Code Playgroud)

因此,假设我们有一系列这些值,我想向制造商ID提取自定义slu的字典.在我将其更改为结构之前,它是一个类,因此原始代码是使用简单的LINQ查询编写的:

ManufacturerValue[] = GetManufacturerValues();
var dict = values.Where(p => !string.IsNullOrEmpty(p.CustomSlug))
                 .ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
Run Code Online (Sandbox Code Playgroud)

我担心的是我想了解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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这将是不好的,因为第三行将创建一个结构的本地副本,这将是缓慢的,因为结构很大,而不会破坏内存总线.我们除了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);
    }
}
Run Code Online (Sandbox Code Playgroud)

那么,有没有人知道代码生成器是否足够智能以使用简单的数组索引,如第二个示例,当生成器代码在结构数组上运行时,或者它是否会实现更天真但更慢的第一个实现?

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

UPDATE

这些变化现已投入生产.事实证明,在重写代码和使用Dot Memory分析器来识别正在使用多少内存以及在哪里,我发现Phalanger PHP编译器代码中存在两个内存泄漏.这是我们的进程使用的内存量持续增长的原因之一,其中一个内存泄漏真的很讨厌,实际上是由Microsoft异步代码引起的(可能值得博客或堆栈溢出问题/答案,以帮助其他人避免它).

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

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

Iva*_*oev 8

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

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

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

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

T Current { get; }
Run Code Online (Sandbox Code Playgroud)

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

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

所以一般来说,在这种情况下你应该避免使用LINQ.

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

var dict = values
    .Where(p => !string.IsNullOrEmpty(p.CustomSlug))
    .ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
Run Code Online (Sandbox Code Playgroud)

您可以struct使用以下方法避免复制:

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

更新:由于似乎对该主题感兴趣,我将为您提供最后一种技术的变体,它可以使您的生活更轻松,同时避免过多的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

(1)它是a struct,但是具有固定大小,因此与正常参考(一个附加int)相比,复制开销可以忽略不计.
(2)您可以struct无需扩展实际数据大小.
(3)你需要对LINQ查询做的就是添加.AsRef()

样品:

var dict = values.AsRef()
    .Where(p => !string.IsNullOrEmpty(p.CustomSlug))
    .ToDictionary(p => p.CustomSlug, p => p.ManufacturerID);
Run Code Online (Sandbox Code Playgroud)

  • 值ref模式非常简洁.我喜欢.我在想类似的东西,这是有道理的.ValueRef是一个结构而不是一个类的原因是什么?我猜它只有两个实际值(引用和索引),所以将它作为一个结构也是有意义的,因为复制它将非常便宜. (2认同)
  • 它是一个名为*Expression bodied members*的C#6**编译器**功能,它只是用于定义属性/方法的糖.它与`public int ManufacturerID {get {return Source [Index] .ManufacturerID; 根本不是lambda.没有魔法:) (2认同)