是 Where 在数组(结构类型)上,经过优化以避免不必要的结构值复制



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

那么有谁知道代码生成器是否足够聪明,可以使用简单的数组索引,就像生成器代码在结构数组上运行的第二个示例一样,还是会实现更幼稚但更慢的第一次实现?

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

更新

这些更改现已投入生产。事实证明,在重写代码和使用点内存分析器来确定正在使用多少内存以及在哪里使用的过程中,我在 Phalanger PHP 编译器代码中发现了两个内存泄漏。这是我们的进程使用的内存量不断增长的原因之一,其中一个内存泄漏非常令人讨厌,实际上是由Microsoft异步代码引起的(可能值得写博客或堆栈溢出问题/答案来帮助其他人避免它)。

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

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

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

无需反编译代码。所有 LINQ to Objects 方法实现都可以在参考源中看到。

关于你的具体问题。使用 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);

结构是 [按值传递][1] - 所以我相当确定,将委托用于您的ToDictionary的行为将导致两个副本,无论其他情况如何。

换句话说,考虑

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

相当于:

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

这显然创建了结构的两个副本以传递给GetKeyGetValue

如果您需要稍微

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

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

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

不幸的是,我对使用 LINQ 来枚举结构一无所知。我通常使用结构来保留少量的值类型。

也许放宽GC会帮助你规避你的表现问题,并给班级另一个机会?我还有一个应用程序,它执行了大量的对象创建和处置,其中性能被 GC 狂热所破坏。使用GCServer="true"解决了这个问题,换来的是使用私有内存的少量增加。

箭头:

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 箭头(闭包)中的数组引用。

编辑:我没有看到伊万·斯托耶夫的回答已经有这个建议。相反,对他的回答投赞成票。

我已经对

Linq的Where()在1000万个结构与大小类上的性能进行了基准测试。

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

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

最新更新