LINQ和Lambdas表达式使用LINQ 和Lambdas表达式是C#语言强大生产力的一个很好体现,但是如果代码需要执行很多次的时候,可能需要对LINQ或者Lambdas表达式进行重写。 例5 Lambdas表达式,List<T>,以及IEnumerable<T>下面的例子使用 LINQ以及函数式风格的代码来通过编译器模型给定的名称来查找符号。
新的编译器和IDE 体验基于调用FindMatchingSymbol,这个调用非常频繁,在此过程中,这么简单的一行代码隐藏了基础内存分配开销。为了展示这其中的分配,我们首先将该单行函数拆分为两行:
第一行中, lambda表达式“s=>s.Name==name” 是对本地变量name的一个 闭包。这就意味着需要分配额外的对象来为 委托对象predict分配空间,需要一个分配一个静态类来保存环境从而保存name的值。编译器会产生如下代码:
两个new操作符(第一个创建一个环境类,第二个用来创建委托)很明显的表明了内存分配的情况。 现在来看看FirstOrDefault方法的调用,他是IEnumerable<T>类的扩展方法,这也会产生一次内存分配。因为FirstOrDefault使用IEnumerable<T>作为第一个参数,可以将上面的展开为下面的代码:
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方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。
代码中并没有使用LINQ扩展方法,lambdas表达式和迭代器,并且没有额外的内存分配开销。这是因为编译器看到symbol 是List<T>类型的集合,因为能够直接将返回的结构性的枚举器绑定到类型正确的本地变量上,从而避免了对struct类型的装箱操作。原先的代码展示了C#语言丰富的表现形式以及.NET Framework 强大的生产力。该着后的代码则更加高效简单,并没有添加复杂的代码而增加可维护性。 Aync异步接下来的例子展示了当我们试图缓存一部方法返回值时的一个普遍问题: 例6 缓存异步方法Visual Studio IDE 的特性在很大程度上建立在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍能够保持Visual Stuido能够响应。下面是获取语法树的第一个版本的代码:
可以看到调用GetSyntaxTreeAsync() 方法会实例化一个Parser对象,解析代码,然后返回一个Task<SyntaxTree>对象。最耗性能的地方在为Parser实例分配内存并解析代码。方法中返回一个Task对象,因此调用者可以await解析工作,然后释放UI线程使得可以响应用户的输入。 由于Visual Studio的一些特性可能需要多次获取相同的语法树, 所以通常可能会缓存解析结果来节省时间和内存分配,但是下面的代码可能会导致内存分配:
代码中有一个SynataxTree类型的名为cachedResult的字段。当该字段为空的时候,GetSyntaxTreeAsync()执行,然后将结果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree对象。问题在于,当有一个类型为Task<SyntaxTree> 类型的async异步方法时,想要返回SyntaxTree的值,编译器会生出代码来分配一个Task来保存执行结果(通过使用Task<SyntaxTree>.FromResult())。Task会标记为完成,然后结果立马返回。分配Task对象来存储执行的结果这个动作调用非常频繁,因此修复该分配问题能够极大提高应用程序响应性。 解决方法: 要移除保存完成了执行任务的分配,可以缓存Task对象来保存完成的结果。
代码将cachedResult 类型改为了Task<SyntaxTree> 并且引入了async帮助函数来保存原始代码中的GetSyntaxTreeAsync()函数。GetSyntaxTreeAsync函数现在使用 null操作符,来表示当cachedResult不为空时直接返回,为空时GetSyntaxTreeAsync调用GetSyntaxTreeUncachedAsync()然后缓存结果。注意GetSyntaxTreeAsync并没有await调用GetSyntaxTreeUncachedAsync。没有使用await意味着当GetSyntaxTreeUncachedAsync返回Task类型时,GetSyntaxTreeAsync 也立即返回Task, 现在缓存的是Task,因此在返回缓存结果的时候没有额外的内存分配。 |