使用几个 .NET 技巧提升你的 LINQ 应用性能






3.58/5 (8投票s)
本文将介绍一种使用 .NET 3.5 和 C# 提升 LINQ 性能的简单方法。
引言
本文将介绍如何使用简单到中等的 C# 代码,极大地提升 LINQ 的性能。
背景
我最近接手了一个项目,需要跟踪个人的人口统计信息,并在修改时,需要将更改同步到另一个数据库。这个要求的难点在于不允许使用触发器,因为被监控的数据库不属于我们。我们只能查询它。第二个难点是,两个数据库无法相互访问。
思路是创建一个 C# 中的通道,为每个人的记录哈希人口统计信息,当发现上次发送的哈希值与人口统计信息哈希值之间存在差异时,会将更改发送给一个进程,该进程会将数据同步到第二个数据库。
本质上,有两组数据:新/修改集和旧集(即第二个数据库中的数据)。如果新/修改集中存在一个旧集中没有的记录,则表示该记录已创建。如果旧集中存在一个新/修改集中没有的记录,则表示该记录已被删除。两个集合的交集,即人员相同但哈希值不同的记录,表示已修改的记录。
使用 LINQ 查询和迭代,并使用 `var` 关键字处理这些集合,传输 18000 条记录大约需要 30 分钟。对于我们的场景,客户可以接受这个时间。但对我来说,这是不可接受的。于是我深入研究了一下,只做了一些小改动,就将 18000 条记录的传输性能从 30 分钟提升到了大约一分钟。
本文中的示例基于我的解决方案。但是,你应该可以轻松地将我的解决方案应用于你可能遇到的任何性能问题。
Using the Code
.NET 3.5 中有一个新的泛型集合,称为 HashSet
。
HashSet
的最佳特性是其内部优化的集合操作。传统上与 LINQ 一起使用的集合并未优化,这也是迭代这些集合耗时的一个原因。加速访问 LINQ 结果的一种方法是将 LINQ 查询的结果存储到 HashSet
中。
要实现这一点,我们必须添加一个 扩展方法 来将 LINQ 结果转换为一个模板化的 HashSet
集合。我们将遵循标准的 IEnumerable<T>
对象,并创建一个静态的 ToHashSet<T>
扩展方法。要创建扩展方法,你必须创建一个包含静态扩展方法的静态类。你要扩展的类型必须是前面加上 `this` 关键字的参数。一旦你执行了下面的代码,任何实现 IEnumerable<T>
的对象都将拥有这个 ToHashSet<T>
方法。
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Text;
namespace LinqImprovements
{
public static class LinqUtilities
{
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> enumerable)
{
HashSet<T> hashSet = new HashSet<T>();
foreach (var en in enumerable)
{
hashSet.Add(en);
}
return hashSet;
}
}
}
接下来我们需要处理一个方法来比较集合中的对象。我们需要定义什么是相等,什么是不相等。因此,我们需要创建一个实现 `IEqualityComparer<T>` 接口的类。
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Text;
namespace LinqImprovements
{
public static class ZIPLinqUtilities
{
public static HashSet<T> ToHashSet<T>(
this IEnumerable<T> enumerable)
{
HashSet<T> hashSet = new HashSet<T>();
foreach (var en in enumerable)
{
hashSet.Add(en);
}
return hashSet;
}
}
public class DemographicHashEqualityComparer :
IEqualityComparer<LastTransmittedPatientDemographic>
{
public bool Equals(LastTransmittedPatientDemographic demographicHashLeft,
LastTransmittedPatientDemographic demographicHashRight)
{
return (demographicHashLeft.PersonProfileId ==
demographicHashRight.PersonProfileId);
}
public int GetHashCode(LastTransmittedPatientDemographic demographicHash)
{
return base.GetHashCode();
}
}
}
当在执行 `Except` 集合操作时,比较存在于 `HashSet` 中的对象,将使用 `DemographicHashEqualityComparer` 对象。集合中具有相同 `PersonProfileId` 的任何对象都被视为相等。这个比较将用于确定自上次同步以来哪些对象已被创建或删除。在实现 `IEqualityComparer<T>` 接口时,你必须实现 `GetHashCode` 方法。在这里,你可以编写自己的哈希方法,或者直接调用基类的方法。
最后一步是创建第二个实现 `IEqualityComparer<T>` 接口的类。这个类将用于在确定自上次同步以来哪些记录已被修改时执行交集逻辑。
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Text;
namespace LinqImprovements
{
public static class ZIPLinqUtilities
{
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> enumerable)
{
HashSet<T> hashSet = new HashSet<T>();
foreach (var en in enumerable)
{
hashSet.Add(en);
}
return hashSet;
}
}
public class DemographicHashEqualityComparer :
IEqualityComparer<LastTransmittedPatientDemographic>
{
public bool Equals(LastTransmittedPatientDemographic demographicHashLeft,
LastTransmittedPatientDemographic demographicHashRight)
{
return (demographicHashLeft.PersonProfileId == demographicHashRight.PersonProfileId);
}
public int GetHashCode(LastTransmittedPatientDemographic demographicHash)
{
return base.GetHashCode();
}
}
public class DemographicHashIntersectComparer :
IEqualityComparer<LastTransmittedPatientDemographic>
{
public bool Equals(LastTransmittedPatientDemographic demographicHashLeft,
LastTransmittedPatientDemographic demographicHashRight)
{
return ((demographicHashLeft.PersonProfileId ==
demographicHashRight.PersonProfileId) &&
(demographicHashLeft.DemographicsHash !=
demographicHashRight.DemographicsHash));
}
public int GetHashCode(LastTransmittedPatientDemographic demographicHash)
{
return base.GetHashCode();
}
}
}
还记得前面我定义已修改记录是那些存在于两个集合中,具有相同人员配置文件 ID 但人口统计哈希值不同的记录吗?这个类将检查这种情况来确定哪些对象已被修改。
在定义了这三个类之后,我们就可以在 LINQ 操作中使用它们了。
在我的例子中,我使用 SQL 查询来构建我的人口统计哈希值,所以结果会返回到一个名为 `newHashValuesTable` 的 `DataTable` 中。我的起点是将这些结果存储到一个 `HashSet<T>` 对象中。
HashSet<LastTransmittedPatientDemographic> newHashValues =
(from pi in newHashValuesTable.AsEnumerable()
select
new LastTransmittedPatientDemographic
{
PersonProfileId = pi.Field<int>("PersonProfileId"),
DemographicsHash = pi.Field<string>("DemographicsHash")
}).ToHashSet<LastTransmittedPatientDemographic>();
这看起来有些冗余。然而,总体来说,将 `DataTable` 结果转换为 `HashSet` 然后查询该集合,比直接访问 `DataTable` 要快。
第二步是获取最后传输的哈希值。这只是一个简单的 LINQ 查询结果,来自一个数据库表。
HashSet<LastTransmittedPatientDemographic> lastTransmittedHashes =
dataContext
.LastTransmittedPatientDemographic
.Select(hash => hash).ToHashSet<LastTransmittedPatientDemographic>();
现在是魔术时间。首先,我们将确定哪些对象已被修改。已修改的对象是两个集合中都存在、具有相同配置文件 ID 但人口统计哈希值不同的对象。还记得 `DemographicHashIntersectComparer` 对象吗?它将为我们完成所有繁重的工作。
DemographicHashIntersectComparer demographicIntersectComparer =
new DemographicHashIntersectComparer();
var updatedPatientInfos = newHashValues.Intersect(lastTransmittedHashes,
demographicIntersectComparer);
LINQ to SQL 有一个重载的 `Intersect` 方法,允许我们传递自己的自定义 `IEqualityComparer<T>` 对象。两个集合中的每个对象都将由我们的自定义比较对象进行比较,那些在比较对象中返回 true 的对象将被放入 `updatedPatientInfos` 对象中。这不是很简单吗?两行代码就确定了已修改的对象。没有 for 循环,没有复杂的 LINQ 查询。而且,由于这是一个 `HashSet`,交集期间执行的集合操作已经得到了优化。
遵循类似的模式,我们将确定已创建的对象。新创建的对象是存在于新哈希值集合中但不存在于最后传输的哈希值集合中的对象。我们可以通过使用 `lastTransmittedHashes` `HashSet` 的 `Except` 方法来确定这一点,使用我们创建的第一个比较对象。`Except` 方法将比较两个集合中的对象,并返回一个结果集,其中第二个集合中的对象从第一个集合中移除。在下面的代码片段中,我们创建了一个新集合,该集合是新哈希值集合中不存在于最后传输的哈希值集合中的对象。
DemographicHashEqualityComparer demographicEqualityComparer =
new DemographicHashEqualityComparer();
var newPatientInfos = newHashValues.Except(lastTransmittedHashes,
demographicEqualityComparer);
最后,确定已删除的对象。这与上面使用相同比较对象的操作正好相反。
DemographicHashEqualityComparer demographicEqualityComparer =
new DemographicHashEqualityComparer();
var deletePatientInfos = lastTransmittedHashes.Except(newHashValues, demo
graphicEqualityComparer);
希望到现在为止,你已经意识到 `HashSet` 扩展使代码变得多么容易和更具可读性。最重要的是,性能测试显示性能提高了 30 倍,因为无需迭代或查询 LINQ 结果集。通过遵循这些步骤,你应该能够将其应用于你自己的 LINQ 项目,并亲身体验性能的提升。
关注点
遗憾的是,如果你使用的是 LINQ to Entities,这种方法对你来说将不适用。原因是 LINQ to Entities 不支持接受比较对象的 `Intersect` 和 `Except` 方法的重载。真可惜,不是吗?
历史
- 2010 年 8 月 3 日 - AJW - 初次创建。
- 2010 年 8 月 4 日 - AJW - 修复了复制粘贴错误。
- 2010 年 8 月 4 日 - AJW - 从扩展方法中移除了对 `.Contains` 的调用。`Add` 方法已经执行了此检查。