Linq to Objects性能优化知多少

最近工作上遇到了一个性能优化的问题,程序批量提交2000行数据,导致将近10分钟才执行完毕。
拿到这样的性能问题,首先是进行Sql Server Profiler监控Sql执行情况。分析可能存在耗时的SQL语句。
通过监控发现耗时最久的Sql语句批量查询6万数据,耗时10s,分析执行计划,也没有优化的点。就转向Visual Studio Profiler看看代码中是否有耗时的操作。一分析不打紧,发现问题尽然出在了Linq语句上,这是为什么呢?且听我娓娓道来。


使用Visual Studio Profiler进行性能诊断

首先讲一下如何使用VS自带的性能分析工具(Visual Studio Profiler)进行性能诊断,默认是通过采样(Sampling)的方式进行性能分析。可以具体根据实际情况,选择性能分析方式。其他性能分析方式,详细参考MSDN Visual Studio Profiler

第一步

添加需要监控的程序集、项目或者是网站


第二步
第三步

附加到进程后,就会开始进行性能监控。默认是通过采样(Sampling)的方式进行性能分析。然后在应用程序上进行业务操作,操作结束后,点击Stop Profiling,就会生成性能报告。

采样方式(Sampling)性能分析术语

Inclusive Samples(非独占样本数):执行目标函数期间收集的样本总数。(包括执行目标函数和其子函数期间收集的样本)
Exclusive Samples (独占样本数): 执行目标函数的指令期间收集的样本总数。(不包含目标函数调用的子函数)
Hot Path(热路径):显示收集数据时执行最活跃的代码路径。
Functions Doing Most Individual Work:执行单个工作最多的函数
Inclusive Samples %(非独占样本百分比): 数值越高说明函数消耗整体资源越多
Exclusive Samples %(独占样本百分比): 数值越高说明函数存在性能瓶颈

看看我代码执行的性能分析报告


采样分析报告

从图中的Hot Path我们可以看到System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()占用了最高的非独占样本百分比,说明程序在这个地方有较高的资源消耗
对.net熟悉的一看就知道这个方法是Linq的枚举迭代器。
那究竟性能瓶颈在哪呢?咱们来看看Functions Doing Most Individual Work占比最高的函数。
点开具体的方法,可以清楚看到存在性能瓶颈的标红代码段。(Vs Profiler就是这么强大)

使用了Linq的匿名函数

使用了Linq的Any()函数
使用Linq的FirstOrDefault()

看完了性能分析报告,那就着手优化吧。


知其然知其所以然,为什么Linq会导致性能瓶颈

首先我们来看一个简单的Linq查询代码片段

class Symbol 
{ 
    public string Name { get; private set; } /*...*/
}
class Compiler 
{ 
    private List<Symbol> symbols; 
    public Symbol FindMatchingSymbol(string name) 
    { 
        return symbols.FirstOrDefault(s => s.Name == name); 
    }
}

为了展示FindMatchingSymbol(string name)函数其中的分配,我们首先将该单行函数拆分为两行:

Func<Symbol, bool> predicate = s => s.Name == name; 
return symbols.FirstOrDefault(predicate);

第一行中,lambda表达式s=>s.Name==name” 是对本地变量name的一个闭包。这就意味着需要分配额外的对象来为委托对象predict分配空间,需要一个分配一个静态类来保存环境从而保存name的值。编译器会产生如下代码:

// Compiler-generated class to hold environment state for lambda 
private class Lambda1Environment 
{ 
    public string capturedName; 
    public bool Evaluate(Symbol s) 
    { 
        return s.Name == this.capturedName;
    } 
}

// Expanded Func<Symbol, bool> predicate = s => s.Name == name; 
Lambda1Environment l = new Lambda1Environment() 
{ 
    capturedName = name
}; 
var predicate = new Func<Symbol, bool>(l.Evaluate);

两个new操作符(第一个创建一个环境类,第二个用来创建委托)很明显的表明了内存分配的情况。
现在来看看FirstOrDefault方法的调用,他是IEnumerable<T>类的扩展方法,这也会产生一次内存分配。因为FirstOrDefault使用IEnumerable<T>作为第一个参数,可以将上面的展开为下面的代码:

// Expanded return symbols.FirstOrDefault(predicate) ... 
IEnumerable<Symbol> enumerable = symbols;
IEnumerator<Symbol> enumerator = enumerable.GetEnumerator(); 
while (enumerator.MoveNext())
{ 
    if (predicate(enumerator.Current)) 
        return enumerator.Current; 
} 
return default(Symbol);

symbols变量是类型为List<T>的变量。List<T>集合类型实现了IEnumerable<T>即可并且清晰地定义了一个迭代器List<T>的迭代器使用了一种结构体来实现。使用结构而不是类意味着通常可以避免任何在托管堆上的分配,从而可以影响垃圾回收的效率。枚举典型的用处在于方便语言层面上使用foreach循环,他使用enumerator结构体在调用推栈上返回。递增调用堆栈指针来为对象分配空间,不会影响GC对托管对象的操作。
在上面的展开FirstOrDefault调用的例子中,代码会调用IEnumerabole<T>接口中的GetEnumerator()方法。将symbols赋值给IEnumerable<Symbol>类型的enumerable变量,会使得对象丢失了其实际的List<T>类型信息。这就意味着当代码通过enumerable.GetEnumerator()方法获取迭代器时,.NET Framework 必须对返回的值(即迭代器,使用结构体实现)类型进行装箱从而将其赋给IEnumerable<Symbol>类型的(引用类型)enumerator变量。
解决方法:
解决办法是重写FindMatchingSymbol方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。

public Symbol FindMatchingSymbol(string name) 
{ 
    foreach (Symbol s in symbols)
    { 
        if (s.Name == name) 
            return s; 
    } 
    return null; 
}

代码中并没有使用LINQ扩展方法,lambdas表达式和迭代器,并且没有额外的内存分配开销。这是因为编译器看到symbolList<T>类型的集合,因为能够直接将返回的结构性的枚举器绑定到类型正确的本地变量上,从而避免了对struct类型的装箱操作。原先的代码展示了C#语言丰富的表现形式以及.NET Framework 强大的生产力。改后的代码则更加高效简单,并没有添加复杂的代码而增加可维护性。


看完以上分析是不是觉得不可思议,我们简单的一个Linq语句最终会让编译器做那么多繁琐的工作。

针对以上分析,对代码进行优化相应优化:

代码优化1
代码优化2

优化后的采样分析报告可以看出System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()的占比从68%降低到了13%。已经大大的优化了程序中Linq存在的性能问题。根据实际测试结果,耗时优化已经降低了一半以上,已经达到了此次代码优化的目的。

优化后采样分析报告

到这里针对Linq的性能优化就结束了。可能读者还会对最终的采样分析报告有疑问,明明还有几个点占比很高啊,为什么不继续优化?
那是因为剩下的采样率都是业务逻辑相关的,只能从业务逻辑上着手优化了。

本文主要参考自.NET程序的性能要领和优化建议

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 版本记录 前言 我们在做app的时候,不是做完功能就结束了,很多时候是需要进行检查和优化的,而xcode自带了一个...
    刀客传奇阅读 2,741评论 0 1
  • 1. 引言 最近一段时间,系统新版本要发布,在beta客户测试期间,暴露了很多问题,除了一些业务和异常问题外,其他...
    圣杰阅读 1,194评论 2 18
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,435评论 25 707
  • 机器学习是做NLP和计算机视觉这类应用算法的基础,虽然现在深度学习模型大行其道,但是懂一些传统算法的原理和它们之间...
    在河之简阅读 20,477评论 4 65
  • 背景 关于python的导入的原因一直不是很理解,上网查了一下这个与命名空间有关。所以写了这样一篇博客梳理其关系。...
    shawnxjf阅读 737评论 0 2