挖掘 ADO.NET Entity 框架的性能

  • Robert Bazinet

2008 年 2 月 23 日

话题:.NET微软DevOps语言 & 开发架构

ADO.NET 团队最近讨论了 ADO.NET Entity 框架的各种性能特征。ADO.NET Entity 框架在 12 月已经进入它的第三个 beta 版本,自那时起开发团队就开始为开发人员提供了使用该框架的相关信息。而现在,则为开发人员提供了框架性能方面的信息。

本文鞭辟入里地介绍了 ADO.NET Entity 框架的性能,演示了如何提高简单查询速度的方法,并阐释了框架的性能特征。

需要重点指出的是,当一个抽象层或者类似 EDM(译注:指 Entity Data Model)的模块被用来转换数据库的关系样式时,会带来一定的性能损失。

查询与结果

本文使用了 NorthWind 数据库作为模型,并创建了一个简单查询:

(NorthwindEntities ne =  NorthwindEntities()) 

{

(Order o  ne.Orders)

    {

i = o.OrderID;

    }

}

测试时,我们的每个查询对整个 848 行数据进行了 10 次遍历。结果很有意思,第 1 次运行时耗费了 4241 毫秒,而接下来的每次运行则平均耗费 13 毫秒左右的时间。最耗时的一部分内容是 ObjectContext 的创建,而在执行任意一个访问数据库的操作时,都会有一些耗时的操作发生。

每次操作的百分比值可以给我们一些启示:

  • 装载元数据(11%)
  • 初始化元数据(14%)
  • 打开连接(8%)
  • 生成视图(56%)
  • 装载程序集(2%)
  • 跟踪(1%)
  • 实例化(7%)
  • 其它(1%)

耗时百分比值最大的是视图生成,它达到了惊人的 56%。既然视图生成是造成性能损耗的罪魁祸首,那么开发人员最好是使用命令行工具EDM 生成器(EdmGen.exe),运行时需要加上视图生成命令参数(/mode:ViewGeneration),它的输出内容为一个代码文件(C# 或者 VB.NET),可以包含在项目中。视图的预生成可以将启动时间降低到 2933 毫秒,而对于循环遍历操作,整个时间可以降低 28%。生成视图并随着应用程序一起发布是提高性能的妙方,但其缺点则在于视图不再是动态的,一旦模型发生改变,就需要重新生成以保持同步。

查询性能

需要指出的是关于性能的主要设计要素是查询缓存。一旦执行了查询,它的一部分内容就被维持在全局缓存中。由于查询与元数据缓存的存在,使得第二次运行的执行速度总是比第一次运行快。例如,如下的 Entity SQL 查询:

(PerformanceArticleContext ne =  PerformanceArticleContext()) 

{

ObjectQuery<Orders> orders = ne.CreateQuery<Orders>();

(Orders o orders)

{

i = o.OrderID;

}

}

第一次运行该查询耗时 179 毫秒,但下一次运行则只耗费了 15 毫秒的时间。首次运行与后续运行在执行方面的区别在于它构建了能够为执行传递 provider 的命令树(command tree)。

LINQ 查询在执行方式上与 Entity SQL 查询相似。例如,下面的查询:

(PerformanceArticleContext ne =  PerformanceArticleContext()) 

{

var orders = from order ne.Orders

select order;

(Orders o orders)

{

i = o.OrderID;

}

}

首次执行 LINQ 查询耗时 202 毫秒,而随后的执行耗时 18 毫秒,两者的差距还要低于 Entity SQL。可以看到,使用编译了的 LINQ 查询对于性能的提高更为明显。编译 LINQ 查询的好处在于它构建了表达树(expression tree),当查询被编译时,后续的执行就不需要重建表达树了。编译的 LINQ 查询代码看起来像这样:

Func<PerformanceArticleContext, IQueryable<Orders>> compiledQuery = CompiledQuery.Compile((PerformanceArticleContext ne) => (from o  ne.Orders select o)); 

(PerformanceArticleContext ne = PerformanceArticleContext())

{

(Orders o compiledQuery(ne))

{

i = o.OrderID;

}

}

注意,PerformanceArticleContext 是一个委托。对于编译了的 LINQ 查询而言,第一次执行耗时 305 毫秒,而随后的执行时间则为 15 毫秒。结果并不惊人,值得关注的是编译的 LINQ 查询比之常规方式的 LINQ 查询,执行时间少了 3 毫秒。或许对于几个查询而言,这算不上什么,但如果有数以千计的查询,这样的性能提升就倍显价值所在了。

ADO.NET 团队建议开发人员在查询中应谨慎使用 Track/NoTrack 选项:

在之前的例子中,所有放在对象创建中的查询结果都被添加到 ObjectStateManager 中,因此我们能够跟踪它们的更新。如果没有必要跟踪对象的更新和删除,那么最好是使用 NoTracking 合并项。例如,在一个 ASP.NET Web 应用程序中,如果它要查询一个指定的分类名称,但却不需要对返回的数据进行更新,那么 NoTracking 就会是一个不错的选择。在这种情形下,使用 NoTracking 的查询会在性能方面得到改善。

基于前面的一组数字,NoTracking 选项能够大幅度地降低执行的时间,而其中性能的提升主要源自于我们停止了对变更的跟踪以及对关系的管理。如果使用 NoTracking 查询,无论是第一次执行还是随后的执行,编译的 LINQ 查询都要优于标准的 LINQ 查询。注意,编译的 LINQ 查询的第二次执行与 Entity SQL 查询的第二次执行相等。

ADO.NET 团队同时还提醒开发者在创建查询时,有一些内容必须铭记于心:

在 Entity 框架中优化查询性能时,应该针对特定的编程场景做出最佳选择。这里列举了几个关键项:

  • ObjectContext 的首次创建包含了装载和验证元数据的性能损耗。
  • 任何一个查询的首次执行都包含了构建一个查询缓存的性能损耗,以利于提高后续查询的执行速度。
  • 编译的 LINQ 查询比未编译的 LINQ 查询要快。
  • 如果不需要跟踪数据的变更与数据的关系,或者对大数据对象进行流操作,那么通过 NoTracking 合并项执行查询,效果会更佳。

若要了解更多关于 ADO.NET 和 Entity 框架的信息,敬请访问ADO.NET 的团队博客

查看英文原文:Digging into the Performance of the ADO.NET Entity Framework
.NET微软DevOps语言 & 开发架构