.NET面试题系列[15]-LINQ:性能
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
.NET⾯试题系列[15]-LINQ:性能
当你使⽤LINQ to SQL时,请使⽤⼯具(⽐如LINQPad)查看系统⽣成的SQL语句,这会帮你发现问题可能发⽣在何处。
提升性能的⼩技巧
避免遍历整个序列
当我们仅需要⼀个资料的时候,我们可以考虑使⽤First / FirstOrDefault / Take / Any等⽅法,它们都会在取得合乎要求的资料后退出,⽽不会遍历整个序列(除⾮最后⼀个资料才是合乎要求的哈哈)。
⽽类似ToList / Max / Last / Sum / Contain等⽅法显⽽易见会遍历整个序列。
例如你判断⼀个集合是否有成员时,请使⽤Any⽽不是Count==0。
因为如果该集合有极多成员时,Count遍历是⾮常消耗时间的。
避免重复枚举同⼀序列
如果你在重复枚举同⼀个序列,你可能会收到如下的警告:
⼀般看到这个提⽰,你需要⼀个ToList/ToDictionary/ToArray等类似的⽅法。
重复枚举是不必要且浪费时间的。
另外,如果程序涉及多线程,或者你的序列含有随机因素,你的每次枚举的结果可能不同。
我们只需要枚举同⼀序列⼀次,之后将结果储存为⼀个泛型集合即可。
例如我们的序列带有随机数:
此时我们会遍历序列四次。
但每次序列都会不同。
例如如果我们呼叫Sum⽅法四次,则可能会出现4个不同的和。
我们必须使⽤ToList⽅法强制LINQ提前执⾏。
避免毫⽆必要的缓存整个序列
在获得序列最后⼀个成员时,我们有很多⽅法:
其中前两个⽅法都不是最好的。
当我们调⽤LINQ的某些⽅法时,我们缓存了整个序列,⽽这可能是不必要的。
我们根本不需要将整个序列留在内存中,只需要获得最后⼀个成员就可以了。
何时使⽤ToList / ToArray / ToDictionary等⽅法
根据前⾯两点,我们可以总结出来何时使⽤ToList / ToArray / ToDictionary等⽅法:
你确定你需要整个序列的时候
你确定你会遍历整个序列多于⼀次的时候
如果序列不是很⼤的时候(因为ToList / ToArray / ToDictionary等⽅法将会在堆上分配⼀个序列对象)
是否返回IEnumerable<T>?
是否返回IEnumerable<T>,或者返回⼀个List,或者数组?注意当你返回IEnumerable<T>时,你并没有开始遍历这个序列(只有当你强制LINQ执⾏时,才会执⾏这个返回IEnumerable<T>的⽅法)。
当然如果数据来⾃远端,你还可以选择IQueryable<T>,它不会把资料⼀股脑拉下来,⽽是做完所有的筛选之后,才ToList,把资料从远端下载下来。
所以在使⽤ORM时,如果它⽤到了IQueryable,请将你的查询也写成表达式⽽不是委托的形式。
参考:
另外,我们可以通过返回IEnumerable<T>⽽不是List或数组,来给予呼叫者最⼤的便利。
(给他⼀个最General类型的返回)
SELECT N+1问题
假设你有⼀个⽗表(例如:汽车),其关联⼀个⼦表,例如轮⼦(⼀对多)。
现在你想对于所有的⽗表汽车,遍历所有汽车,然后打印出来所有轮⼦的信息。
默认的做法将是:
SELECT CarId FROM Cars;
然后对于每个汽车:
SELECT * FROM Wheel WHERE CarId = ?
这会SELECT 2个表⼀共N(⼦表的⾏数)+1(⽗表)次,故称为SELECT N+1问题。
考察下⾯的代码。
假设album是⼀个表,artist是另外⼀个表,album和artist是⼀对多的关系:
我们知道foreach会强制LINQ执⾏,于是,我们可以想象这也是⼀个SELECT N+1问题的例⼦:先获得所有album(SELECT * FROM ALBUM),然后遍历,对每⼀个album的Title,检查其是否包含关键字,如果符合,再去SELECT 表artist,共SELECT N+1次。
我们可以通过LINQPAD或其他⽅式检查编译器⽣成的SELECT语句数⽬,⼀定会是N+1条SQL语句。
解决⽅法:使⽤⼀个匿名对象作为中间表格,预先将两个表join到⼀起:
⽣成的SQL将只有⼀句话!
中的第三点,就是⼀个典型的SELECT N+1问题。
在代码中,选择了前100个score(⼀条SQL),然后对所有score进⾏遍历,从表Student 中获得Name的值(100条SQL)。
解决⽅法也在⽂章中给出了,就是将两个表连到⼀起。
该⽂章的“联表查询统计”这⼀节,说的还是这个问题。
简单说,还是每次都⽤LINQPad⼯具,看看最终⽣成的SQL到底长啥样。
(当然还有很多其他⼯具,或者最基本的就是⽤SQL Profiler不过⽐较⿇烦)
LINQ to SQL的性能问题
提升从数据库中拿数据的速度,可以参考以下⼏种⽅法:
1. 在数据库中的表中定义合适的索引和键
2. 只获得你需要的列(使⽤ViewModel或者改进你的查询)和⾏(使⽤IQueryable<T>)
3. 尽可能使⽤⼀条查询⽽不是多条
4. 只为了展⽰数据,⽽不进⾏后续修改时,可以使⽤AsNoTracking。
它不会影响⽣成的SQL,但它可以令系统少维护很多数据,从⽽提
⾼性能
5. 使⽤Reshaper等⼯具,它可能会在你写出较差的代码时给出提醒
我们可以通过很多⼯具来获得系统产⽣的SQL语句,例如LINQPAD或者SQL Profiler。
在EF6中,我们还可以使⽤这样的⽅法:
注意:编译器不⼀定能够将你的LINQ语句翻译为SQL,例如字符串的IndexOf⽅法就不被⽀持。
使⽤LinqOptimizer提升LINQ语句的性能
LinqOptimizer可以通过nuget获得。
你可以通过在IEnumerable<T>上调⽤AsQueryExpr⽅法来令LinqOptimizer优化你的LINQ语句。
使⽤Run⽅法执⾏:
LINQ:替代选择
在没有找到性能瓶颈之前,不要过早优化。
1. 是否存在需要长时间运⾏的LINQ语句?
2. 是否在数据库上取得数据,并运⾏LINQ语句?(这意味着存在⼀个LINQ语句到SQL的表达式转换)
3. 数据规模是否巨⼤?
4. 是否需要重复极其多次运⾏相同的LINQ语句?
LINQ VS Foreach(重复极其多次运⾏相同的LINQ语句)
在什么情况下,LINQ反⽽不如Foreach表现好?两者的性能差距是怎样的?下⾯的例⼦的序列有⼀千万个成员,我们对它们做些简单运算。
结果:
可以看到Foreach的表现稍好⼀点。
LINQ的额外开销在于将lambda表达式转换为委托的形式,⽽foreach不需要。
虽然这⼀点点额外开销对于普通的情况基本可以忽略,但如果重复⼀千万次,则性能可能会有较为明显的差异。
LINQ VS PLINQ(重复运⾏相同的LINQ语句)
显⽽易见,如果我们重复运⾏相同的任务,且任务之间⼜没有什么关系(不需要对结果进⾏汇总),此时我们可以想到⽤多线程来解决问题,重复利⽤系统的资源:
执⾏后只⽤了423毫秒。
通常来说,执⾏的结果将等于Foreach的时间,除以系统CPU的核数量。
当CPU为双核时,速度⼤概可以提升⼀倍。
当然,对于单核机器来说,PLINQ是没有意义的。
当你的机器拥有多核,并且你处理相同的任务时(例如从不同的⽹站下载内容,并做相同的处理),可以考虑使⽤PLINQ。
不过PLINQ也需要⼀些额外开销:它访问线程池,新建线程,将任务分配到各个线程中,然后还要收集任务的结果。
所以,你需要测量PLINQ是否真的可以加快你的代码的运⾏速度。
⾃定义ORM
通常,只有在如下情况下才会考虑将⾃⼰写的ORM投⼊⽣产使⽤:
存在⼀些特定的复杂查询,在项⽬中⼴泛出现,此时⾃⼰写的ORM做了很多优化,表现好于EF
存在⼀些特定的业务逻辑,例如将表达式解析为XML等,EF没有对应的功能
你的项⽬对性能要求达到了⾮常苛刻的程度,导致EF的⼀些性能可以接受的⽅法在你这⾥变成了不能接受。
例如EF使⽤了反射,但如果你的ORM只⽤于你开发的软件,所有的情况你都可以事先预计,那你也可以不⽤反射
⽽⼤部分ORM开发出来的⽬标仅仅是:
令查询语法更加接近SQL
加⼊了若⼲语法糖或代码⽣成快捷⽅式,令编写代码速度稍微加快
性能和EF相差⽆⼏,有些甚⾄还不如EF
没有经过彻底的测试
⾃学使⽤
通常,⾃⼰开发⼀套ORM需要很长的时间,才能保证没有错误,并⽤于⽣产环境。
⼤部分情况下,EF已经是⼀个不错的选择。
性能是双刃剑,它可能也会毁了你的代码,让你的代码难以维护。
LINQ性能问题:总结
使⽤LINQPad等⼯具观察⽣成的SQL。
当你优化之后,再次在LINQPad上运⾏看看是否造成了可观的性能提升。
是否需要在数据库上筛选数据,并运⾏LINQ语句?如果是的话,考虑返回IQueryable<T>,并考察编译器构建的中间SQL语句。
数据规模是否巨⼤?避免过早的ToList,返回IEnumerable/ IQueryable<T>类型的巨⼤规模的数据。
是否需要重复极其多次运⾏相同的LINQ语句?考虑使⽤foreach或者PLINQ来优化性能。
使⽤LinqOptimizer来优化LINQ语句。
使⽤Reshaper等⼯具,它可能会在你写出较差的代码时给出提醒。
上MSDN,nuget查询是否已经有了现成的⽅法(例如获得最后⼀个元素)。
撰写单元测试来保证你的优化的正确性。