【转】未经授权,请勿转载
EntityFramework规范
- Entity Framework版本规范
- Entity Framework 6.0 Native Image调优
- Entity Framework T4 Template调优
- Entity Framework查询过程分析
- Entity Framework View生成调优
- Entity Framework只读查询调优
- Entity Framework方法性能分析
- Entity Framework Async编程
- Entity Framework其他优化
Entity Framework版本规范
Entity Framework版本:6.1.0及以上。
.NET Framework 4.5.1及以上
如有EF 6以下的情况,私聊相关调优问题。以后不建议使用EF6以下版本。
Entity Framework 6.0Native Image调优
一般情况下在.NET环境下生成的程序集都是托管代码,在运行环境,MSIL托管代码到本地Native代码之间有一个本地代码生成阶段,这是由JIT编译器在每次程序启动时触发。由于.NETFramework自带的基础类库,都做了NativeImage,所以不会有启动性能损耗的感觉,但是自有的Assembly就必须经历这个一个NativeCode Generation的过程。
在Entity Framework 5.0之前,EF组件都是随System.Data.Entity发布,这是一个.NETFramework原生组件,已经注册了NativeImage。但是到了EntityFramework 6,EF小组为了提高包更新自由度和发布频度,将EF的核心功能提取到了EntityFramework.dll和EntityFramework.SqlServer.dll这2个程序集中,与.NETFramework完全独立,所以在程序每次启动之时,会触发NativeCode Generation,直观感觉就是程序启动速度很慢。下面我们通过命令的形式,手动将ManagedCode注册成NativeCode。
NGen.exe(Native Image Generator)使用JIT编译器生成NativeCode Image,并将生成的Image存储到本地硬盘(确切的说是GAC目录下一个特定子目录)。当程序需要某个程序集时,CLR加载NativeImage,而不是原始程序集。
下面我们通过一个程序演示一下Native Image模式下EF程序的加载速度区别。
首先创建一个简单EF项目。如下:
代码很简单,通过SQLQuery使用原生T-SQL获取一条数据。
默认,我的本地测试环境未注册EF 6Native Image。执行情况如下:
第一次加载EF,花了463ms。
通过NGen对EF 6的2个程序集EntityFramework.dll和EntityFramework.SqlServer.dll(注意:32位和64位是不同的程序集,需要分别注册)进行注册后,再次运行程序。执行情况如下:
第一次加载明显加快,花了172ms。由于我使用了SqlQuery,比较原始的SQL原语,如果使用ObjectContext和DBContext的话,启动性能将会有10倍左右的提升。
既然NGen这么神奇,那么我们一起来学一下NGen是怎么使用的。
首先,是将程序集Native Image化,并将NativeImage注册到缓存。
很简单,通过NGen install <程序集路径>即可将相关程序集的NativeImage进行注册(注意,同一个程序集,不管路径是否相同,重复注册,没有任何卵用,当然也不会报错),注意管理员权限运行Command哦。
接着,验证Native Image是否注册成功。
通过NGen display <程序集路径>即可验证。
全部工作就这么多。提个问题?我们注册的Native Image存在硬盘的哪里呢?给个提示GAC目录~~下的某个目录~~。有兴趣的童鞋自已研究分享。
Entity Framework T4Template调优
在DataBase First模式下,通过数据库生成EDMX,再通过T4Template生成Context类和Entity类。由于CodeFirst模式下,功能有限,且在处理过程中也是先生成EDMX再进行处理,性能较DataBaseFirst有所下降,所以项目中禁绝用CodeFirst模式进行编程,任何理由都不可以!
在Entity Framework中有2个T4Template。如图:
一个是Context模板,生成DBContext派生类,提供数据库操作、监控和管理接口。一个是Entity模板,基于EDMX生成Entity类,提供强类型实体。
在开始T4 Template优化之前,先说说EntityFramework中的性能关注点:
1、Lazy Loading和Eager Loading:EF作为一个强大的ORM框架,提供了实体关系查询的功能。一个是Lazy Loading,在需要使用关联实体时由EF自动加载关联实体,这种模式有一个问题,一是Loading过程不可控,另一个是按需加载,会导致多次数据库查询;另一个是EagerLoading通过Include的方式构建类似Join语句进行查询,这种模式问题在于如果表过多,导致低性能。说了这么多,其实我要说的是,项目中必须杜绝LazyLoading和EagerLoading,两者都不是好东西。从数据库的设计面来说,支持范式是为了保证数据完整性和杜绝数据冗余,但是在生成数据库时引入外键,会导致项目的维护性急剧下降,在高并发的情况下,性能更成问题。所以外键到数据设计文档为止,为后人留下设计依据即可,项目中的数据完整性通过代码保证,可提供最佳性能。所以,既然没有Relation,LazyLoading和EagerLoading都是浮云。
2、Change Tracking:EF中Entity有如下几种状态:
- Added
- Deleted
- Modified¡
- Unchanged¡
- Detached
通过DBContext或者ObjectContext提供的操作方法,可以实现Entity状态变迁,最终体现在持久化过程。对于Entity的实现,EF中支持2中模式:一种是POCOEntity,另一种是ChangeTracking Proxy(也有另一种叫法:POCOEntity wrapped by a proxy)。
POCO Entity全称叫Plain-Old CLR Object Entity,是一种纯自然态.NET对象,没有任何附加,对于这种对象EF在做ChangeTracking时,需要遍历当前对象的所有属性和原始副本对象的所有属性进行判断,最终确认5中状态中的一种,这个耗时的过程在调用DbContext.SaveChanges时都会触发(假设AutoDetectChangesEnabled没有关闭)。可以想象Entity越复杂,Tracking的耗费越大。
另一种是Change Tracking Proxy,这种对象会在内存中生成一个继承于POCOEntity类的Proxy类,该类会修改每个Property,附加状态变更事件处理,任何针对Property的修改都会实时通知到ObjectState Manager,到时候把AutoDetectChangesEnabled一关,就可以实现实时状态变更通知。那么如何实现ChangeTracking Proxy?很简单,就是在原来POCOEntity上针对每个Property,将之修改为Virtual即可。这种模式也有一个缺陷,就是增加事件通知的开销。
总体来说,如果你的Entity很多,每个Entity字段又比较复杂,可以使用ChangeTracking Proxy,否则POCOEntity也不会慢多少。
其实,EF中的性能关注点有很多,这里指出的这2点,可以通过修改T4Template(LazyLoading可以通过VS进行配置)进行变更。
首先,是禁用Lazy Loading。双击Test.edmx(不同项目,名称不一样,自行识别) ,打开设计器,右键,选择“Properties”,打开“Properties”设计框,将“LazyLoading Enabled”置为“False”即可。如图:
设置完后,再看Context模板生成的代码,如图:
多个禁用Lazy Loading的配置项。
接着,是将POCO Entity模式切换成ChangeTracking Proxy模式。
第一步是将AutoDetectChangesEnabled关闭,这个需要修改Context.tt模板,打开Context.tt文件,增加如下代码,如图:
保存Context.tt后,生成的Context.cs会增加如下代码,如图:
第二步是将生成Change Tracking Proxy Entity,打开Test.tt(不同项目不一样,自行识别),找到如下代码,在字符串中添加“virtual”,如图:
保存Test..tt后,生成的Test1.cs会增加如下代码,如图:
到此,EF会实时跟踪Entity属性变更,不会在SaveChanges时对DetectChanges检查,但是Entity状态不会丢失。
Entity Framework查询过程分析
EF进行第一次查询时需要做很多事情,比如模型加载,验证,视图生成等。所以第一次查询也被称为“冷”查询。相应的,后续的查询被称为“热”查询,速度更快。“冷”查和“热”查的执行过程分析如下。
First Query Execution – cold query
Code User Writes | Action | EF4 Performance Impact | EF5 Performance Impact | EF6 Performance Impact |
| Context creation | Medium | Medium | Low |
|
|
|
|
|
| Query expression creation | Low | Low | Low |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| LINQ query execution |
|
|
|
| - Metadata loading | High but cached | High but cached | High but cached |
| - View generation | Potentially very high but cached | Potentially very high but cached | Medium but cached |
| - Parameter evaluation | Medium | Low | Low |
| - Query translation | Medium | Medium but cached | Medium but cached |
| - Materializer generation | Medium but cached | Medium but cached | Medium but cached |
| - Database query execution | Potentially high | Potentially high (Better queries in some situations) | Potentially high (Better queries in some situations) |
| + Connection.Open |
|
|
|
| + Command.ExecuteReader |
|
|
|
| + DataReader.Read |
|
|
|
| - Object materialization | Medium | Medium | Medium (Faster than EF5) |
| - Identity lookup | Medium | Medium | Medium |
| Connection.Close | Low | Low | Low |
Second Query Execution – warm query
Code User Writes | Action | EF4 Performance Impact | EF5 Performance Impact | EF6 Performance Impact |
| Context creation | Medium | Medium | Low |
|
|
|
|
|
| Query expression creation | Low | Low | Low |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| LINQ query execution |
|
|
|
| - Metadata lookup |
|
|
|
| - View lookup |
|
|
|
| - Parameter evaluation | Medium | Low | Low |
| - Query lookup | Medium |
|
|
| - Materializer lookup |
|
|
|
| - Database query execution | Potentially high | Potentially high (Better queries in some situations) | Potentially high (Better queries in some situations) |
| + Connection.Open |
|
|
|
| + Command.ExecuteReader |
|
|
|
| + DataReader.Read |
|
|
|
| - Object materialization | Medium | Medium | Medium (Faster than EF5) |
| - Identity lookup | Medium | Medium | Medium |
| Connection.Close | Low | Low | Low |
有很多方法可提高“冷”查和“热”查的性能,比如,我们可以通过View预生成来提高模型加载速度。
Entity Framework View生成调优
何为View?在EF的执行过程中,任何一个操作都会经历如下过程:
1、将相关Query View(或UpdateView)合并成一个完成View。
2、将合并View通过计划编译器后,产生存储可以识别的表现形式(比如T-SQL)
在该过程中,View的合并和编译在每个操作过程中都会执行,视整个模型的复杂程度,性能会各有不同。说白了,最终生成一个SQL,如果可以预先生成这个SQL,就可以避免不必要的性能损耗,这个过程叫“Pre-GenerationView”。
预生成View,可以通过EntityFramework Power Tools工具(下载地址:https://visualstudiogallery.msdn.microsoft.com/72a60b14-1581-4b9b-89f2-846072eff19d/,可惜官方不支持2015,我自己打了个包,可以支持2015,地址:EFPowerTools15.vsix)比较方便的实现。
下载后是一个VSIX扩展,按向导安装。安装完后在VS中打开项目,找到EDMX文件,右键,按图所示,完成操作:
完成后会在当前项目下,生成一个View.cs文件,该文件就是预编译的View脚本,打开就是各种脚本。如果你看不惯自动生成的脚本,改之,优化到你想要的方式为止,呵呵。
Entity Framework只读查询调优
刚刚讲完Change Tracking,对EF Entity状态跟踪有个大概了解,就是通过ObjectState Manager进行状态跟踪管理, 不管是POCOEntity还是ChangeTracking Proxy,都逃不出ObjectState Manager监管。但是,有时候我们只需要以“只读”形式获取数据,不做任何操作,这种查询称为“NoTracking Query”。有2中性能较优的方式实现查询:一种是通过DbContext.Database.SqlQuery<T>("sql"),另一种是通过DbSet.AsNoTracking()。
DbContext.Database.SqlQuery方式:
DbContext.Database.SqlQuery
using (var context = new TestEntities())
{
var test1Entity =
context.Database.SqlQuery<Test1>("select top 1 * from Test1 with(nolock)")
.FirstOrDefault();
Console.WriteLine("{0}-{1}-{2}", test1Entity.KeyID, test1Entity.Name, test1Entity.Age);
}
DbSet.AsNoTracking方式:
DbSet.AsNoTracking
using (var context = new TestEntities())
{
var test1Entity = context.Test1.AsNoTracking().Take(1).FirstOrDefault();
Console.WriteLine("{0}-{1}-{2}", test1Entity.KeyID, test1Entity.Name, test1Entity.Age);
}
在这2中模式下,任何对Entity的修改都不会触发状态变更。从性能上看,SqlQuery较优,但是要手动写SQL;
Entity Framework方法性能分析
在开始EF方法性能分析之前,先普及一下EF支持哪些形式的查询,各种查询的优缺点。
- LINQ to Entities(建议使用)
- No Tracking LINQ to Entities(在“只读”查询时建议使用)
- Entity SQL over an ObjectQuery(不建议使用,需要配合ObjectContext使用)
- Entity SQL over an EntityCommand(不建议使用,太原始,几乎就是ADO.NET原生方法的简单封装)
- SqlQuery and ExecuteStoreQuery(在性能要求特别高的情况建议使用)
- CompiledQuery(不建议使用,编程难度大)
LINQ to Entities
LINQ to Entities
var test1Entity = context.Test1.Where(testEntity => testEntity.Name == "EF").FirstOrDefault();
优点:
- 适用于C(reate)U(pdate)D(elete)操作,有状态跟踪。
- 支持语法糖。
- 性能良好。
缺点:
- 不支持复杂查询语法(其实也不建议用复杂语法,害己害人)
No Tracking LINQ toEntities
No Tracking LINQ to Entities
var test1Entity = context.Test1.AsNoTracking().Where(testEntity => testEntity.Name == "EF").FirstOrDefault();
优点:
- 比LINQ to Entities性能高好多。
- 支持语法糖。
缺点:
- 不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
- 不支持复杂查询语法。
SqlQuery andExecuteStoreQuery
有3中方式:
- SqlQuery on Database:
SqlQuery on Database
var test1Entity = context.Database.SqlQuery<Test1>("select top 1 * from Test1 with(nolock)").FirstOrDefault();
- SqlQuery on DbSet:
SqlQuery on DbSet
var test1Entity = context.Test1.SqlQuery("select top 1 * from Test1 with(nolock)").FirstOrDefault();
- ExecuteStoreQuery:
ObjectContxt模式下方式,不建议采用,以后建议在DbContext模式下编程。当然性能吗,是所有方式中最好的。
优点:
- 通常来说,在DbContext系列中是性能最好的方法。
- DbSet.SqlQuery适用于C(reate)U(pdate)D(elete)操作,有状态跟踪。
缺点:
- 文本SQL,不支持多数据类型(SQL Server,Oracle,MySql),而且无法进行编译时验证。
- Database.SqlQuery不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
- ObjectContext.ExecuteStoreQuery不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
下面是网上有人做的基线测试,比对各种查询之间的性能。
EF | Test | Time (ms) | Memory |
EF5 | ObjectContext Entity Command | 621 | 39350272 |
EF5 | DbContext Sql Query on Database | 825 | 37519360 |
EF5 | ObjectContext Store Query | 878 | 39460864 |
EF5 | ObjectContext Linq Query No Tracking | 969 | 38293504 |
EF5 | ObjectContext Entity Sql using Object Query | 1089 | 38981632 |
EF5 | ObjectContext Compiled Query | 1099 | 38682624 |
EF5 | ObjectContext Linq Query | 1152 | 38178816 |
EF5 | DbContext Linq Query No Tracking | 1208 | 41803776 |
EF5 | DbContext Sql Query on DbSet | 1414 | 37982208 |
EF5 | DbContext Linq Query | 1574 | 41738240 |
|
|
|
|
EF | Test | Time (ms) | Memory |
EF6 | ObjectContext Entity Command | 480 | 47247360 |
EF6 | ObjectContext Store Query | 493 | 46739456 |
EF6 | DbContext Sql Query on Database | 614 | 41607168 |
EF6 | ObjectContext Linq Query No Tracking | 684 | 46333952 |
EF6 | ObjectContext Entity Sql using Object Query | 767 | 48865280 |
EF6 | ObjectContext Compiled Query | 788 | 48467968 |
EF6 | DbContext Linq Query No Tracking | 878 | 47554560 |
EF6 | ObjectContext Linq Query | 953 | 47632384 |
EF6 | DbContext Sql Query on DbSet | 1023 | 41992192 |
EF6 | DbContext Linq Query | 1290 | 47529984 |
从基线测试来看,ObjectContext整体性能要优于DbContext,有能力的童鞋可以使用DbContext的ObjectContext包装进行编程,建议还是DbContext,比较简单。从EF 5到EF6,性能上提高了,相对的内存使用量也多了。
Entity Framework Async编程
在开始EF Async编程前,先简单说一下异步编程模式,有APM、EAP、async/await模式3种,具体原理各自翻MSDN(async/await:https://msdn.microsoft.com/zh-cn/library/hh191443.aspx)。有几点需要说明一下,异步模式不会提高程序的性能,绝对不会,它提高的是程序的响应能力,对Web来说还有一个更专业的说法叫,提高Web的吞吐量;其次,异步模式不是多线程,它也不会开启多个线程去处理任务(好吧,绝大多数时候不会开启其他线程去处理),它只会在当前线程上下文中记录异步信息,待到异步操作(一般都是IO)完成后,通知当前线程(或者到线程池中找一个未使用的线程)完成后续工作。千万不要将异步和多线程搞混了。异步编程的目的旨在将程序中线程等待的空余时间利用起来,让线程不至于傻等,它不是戏法,如果你的线程从头到尾都是满满当当的计算密集型工作,异步编程对你毫无意义,请注意这一点,额,也不算毫无意义,鉴于现在框架太牛逼,人家还是会帮你执行下,但是会重新开一个线程,重新开一个线程哦,然后呢,然后,你的CPU就爆表了,^O^ 。像Redis、NodeJs这种单线程的程序可以高并发,秘诀就在这里,当然Linux下人家叫NIO,Windows也有一个不错的名字叫非阻塞IO。
下面开始正题,先通过一段代码演示一下EF Async效果。
Demo
class Program
{
static private void Log(string message)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} ThreadID:{Thread.CurrentThread.ManagedThreadId} : " + message);
}
private static void Main(string[] args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
Log("A : 我要开始干活了...");
Log("A : ......");
Log("A : 报表中要放一个测试数据...");
Log("A : 喂,B你去给我拿下测试数据...");
#region Async Operation
var retriveDataTask = RetriveDataAsync();
#endregion
Log("A : 接着苦逼干活...");
Log("A : ......");
retriveDataTask.Wait();
Log("A : 将测试数据插入报表...测试数据:" + retriveDataTask.Result.Name);
}
static async Task<Test1> RetriveDataAsync()
{
Log("B : B去拿测试数据...");
using (var context = new TestEntities())
{
var test1EntityTask = await context.Test1.SqlQuery("select top 1 * from Test1 with(nolock)").FirstOrDefaultAsync();
Log("B : A,已经拿到数据了...");
return test1EntityTask;
}
}
}
执行效果如下:
从执行结果可以看出在B开始处理到B处理结束期间,A一直在处理数据,有一点要注意,就是B处理的线程是6,A是1,但这是随机的,有可能A和B都是同一线程,看主线程的空闲情况,也就是说EFAsync操作是可以在同一线程上执行的。在代码中我使用了FirstOrDefaultAsync来启动EF的异步模式,EF还有其他异步方法:SaveChangesAsync、ToListAsync、FindAsync等等。本质上,就是对async/awaitPattern的简单应用,前提还是要明白async/awaitPattern的工作原理和编程模式。其他更详细的应用,请自行Googling。
Entity Framework其他优化
- Context存活周期尽量短。DBContext(或者ObjectContext)在EF被设计为轻量级对象,说白了,就是反复回收重建,并不会占用太多资源,所以不要将Context的生命周期延长,用完即丢。
· GC服务端配置。 以前的CLR中GC模式是有一个线程专门负责回收垃圾,这个线程在对垃圾对象遍历做标记时,会锁定所有应用的线程。后来,为了提高性能,增加了ServerGC模式。在Server GC模式下,会针对每个CPU(其实是核(Core)),专门有一个线程负责回收当前CPU上线程的资源,锁也只锁当前CPU上的线程,提高垃圾回收效率,减少线程锁定时长,但只对服务端程序起作用(何为服务端程序?能让Web.config起作用的都是)。这个配置也不只是针对EF啦,原则上所有的服务端配置都应遵循。配置如下,直接在Web.config中添加即可:
Server Garbage Collection
<configuration>
<runtime>
<gcServer enabled="true" />
</runtime>
</configuration>