想了解更多关于新的编译器的信息,能够访问 .NET Compiler Platform ("Roslyn")程序员
在对.NET 进行性能调优以及开发具备良好响应性的应用程序的时候,请考虑如下这些基本要领:算法
编写代码比想象中的要复杂的多,代码须要维护,调试及优化性能。 一个有经验的程序员,一般会对天然而然的提出解决问题的方法并编写高效的代码。 可是有时候也可能会陷入过早优化代码的问题中。好比,有时候使用一个简单的数组就够了,非要优化成使用哈希表,有时候简单的从新计算一下能够,非要使用复杂的可能致使内存泄漏的缓存。发现问题时,应该首先测试性能问题而后再分析代码。数组
剖析和测量不会撒谎。测评能够显示CPU是否满负荷运转或者是存在磁盘I/O阻塞。测评会告诉你应用程序分配了什么样的以及多大的内存,以及是否CPU花费了不少时间在 垃圾回收上。缓存
应该为关键的用户体验或者场景设置性能目标,而且编写测试来测量性能。经过使用科学的方法来分析性能不达标的缘由的步骤以下:使用测评报告来指导,假设可能出现的状况,而且编写实验代码或者修改代码来验证咱们的假设或者修正。若是咱们设置了基本的性能指标而且常常测试,就可以避免一些改变致使性能的回退(regression),这样就可以避免咱们浪费时间在一些没必要要的改动中。性能优化
好的工具可以让咱们可以快速的定位到影响性能的最大因素(CPU,内存,磁盘)而且可以帮助咱们定位产生这些瓶颈的代码。微软已经发布了不少性能测试工具好比: Visual Studio Profiler, Windows Phone Analysis Tool, 以及 PerfView.数据结构
PerfView是一款免费且性能强大的工具,他主要关注影响性能的一些深层次的问题(磁盘 I/O,GC 事件,内存),后面会展现这方面的例子。咱们可以抓取性能相关的 Event Tracing for Windows(ETW)事件并能以应用程序,进程,堆栈,线程的尺度查看这些信息。PerfView可以展现应用程序分配了多少,以及分配了何种内存以及应用程序中的函数以及调用堆栈对内存分配的贡献。这些方面的细节,您能够查看随工具下载发布的关于PerfView的很是详细的帮助,Demo以及视频教程(好比 Channel9上的视频教程)多线程
你可能会想,编写响应及时的基于.NET的应用程序关键在于采用好的算法,好比使用快速排序替代冒泡排序,可是实际状况并非这样。编写一个响应良好的app的最大因素在于内存分配,特别是当app很是大或者处理大量数据的时候。闭包
在使用新的编译器API开发响应良好的IDE的实践中,大部分工做都花在了如何避免开辟内存以及管理缓存策略。PerfView追踪显示新的C# 和VB编译器的性能基本上和CPU的性能瓶颈没有关系。编译器在读入成百上千甚至上万行代码,读入元数据活着产生编译好的代码,这些操做其实都是I/O bound 密集型。UI线程的延迟几乎所有都是因为垃圾回收致使的。.NET框架对垃圾回收的性能已经进行太高度优化,他可以在应用程序代码执行的时候并行的执行垃圾回收的大部分操做。可是,单个内存分配操做有可能会触发一次昂贵的垃圾回收操做,这样GC会暂时挂起全部线程来进行垃圾回收(好比 Generation 2型的垃圾回收)app
这部分的例子虽然背后关于内存分配的地方不多。可是,若是一个大的应用程序执行足够多的这些小的会致使内存分配的表达式,那么这些表达式会致使几百M,甚至几G的内存分配。好比,在性能测试团队把问题定位到输入场景以前,一分钟的测试模拟开发者在编译器里面编写代码会分配几G的内存。框架
装箱发生在当一般分配在线程栈上或者数据结构中的值类型,或者临时的值须要被包装到对象中的时候(好比分配一个对象来存放数据,活着返回一个指针给一个Object对象)。.NET框架因为方法的签名或者类型的分配位置,有些时候会自动对值类型进行装箱。将值类型包装为引用类型会产生内存分配。.NET框架及语言会尽可能避免没必要要的装箱,可是有时候在咱们没有注意到的时候会产生装箱操做。过多的装箱操做会在应用程序中分配成M上G的内存,这就意味着垃圾回收的更加频繁,也会花更长时间。
在PerfView中查看装箱操做,只须要开启一个追踪(trace),而后查看应用程序名字下面的GC Heap Alloc 项(记住,PerfView会报告全部的进程的资源分配状况),若是在分配相中看到了一些诸如System.Int32和System.Char的值类型,那么就发生了装箱。选择一个类型,就会显示调用栈以及发生装箱的操做的函数。
下面的示例代码演示了潜在的没必要要的装箱以及在大的系统中的频繁的装箱操做。
public class Logger { public static void WriteLine(string s) { /*...*/ } } public class BoxingExample { public void Log(int id, int size) { var s = string.Format("{0}:{1}", id, size); Logger.WriteLine(s); } }
这是一个日志基础类,所以app会很频繁的调用Log函数来记日志,可能该方法会被调用millons次。问题在于,调用string.Format方法会调用其 重载的接受一个string类型和两个Object类型的方法:
String.Format Method (String, Object, Object)
该重载方法要求.NET Framework 把int型装箱为object类型而后将它传到方法调用中去。为了解决这一问题,方法就是调用id.ToString()和size.ToString()方法,而后传入到string.Format 方法中去,调用ToString()方法的确会致使一个string的分配,可是在string.Format方法内部不论怎样都会产生string类型的分配。
你可能会认为这个基本的调用string.Format 仅仅是字符串的拼接,因此你可能会写出这样的代码:
var s = id.ToString() + ':' + size.ToString();
实际上,上面这行代码也会致使装箱,由于上面的语句在编译的时候会调用:
string.Concat(Object, Object, Object);
这个方法,.NET Framework 必须对字符常量进行装箱来调用Concat方法。
解决方法:
彻底修复这个问题很简单,将上面的单引号替换为双引号即将字符常量换为字符串常量就能够避免装箱,由于string类型的已是引用类型了。
var s = id.ToString() + ":" + size.ToString();
下面的这个例子是致使新的C# 和VB编译器因为频繁的使用枚举类型,特别是在Dictionary中作查找操做时分配了大量内存的缘由。
public enum Color { Red, Green, Blue } public class BoxingExample { private string name; private Color color; public override int GetHashCode() { return name.GetHashCode() ^ color.GetHashCode(); } }
问题很是隐蔽,PerfView会告诉你enmu.GetHashCode()因为内部实现的缘由产生了装箱操做,该方法会在底层枚举类型的表现形式上进行装箱,若是仔细看PerfView,会看到每次调用GetHashCode会产生两次装箱操做。编译器插入一次,.NET Framework插入另一次。
解决方法:
经过在调用GetHashCode的时候将枚举的底层表现形式进行强制类型转换就能够避免这一装箱操做。
((int)color).GetHashCode()
另外一个使用枚举类型常常产生装箱的操做时enum.HasFlag。传给HasFlag的参数必须进行装箱,在大多数状况下,反复调用HasFlag经过位运算测试很是简单和不须要分配内存。
要牢记基本要领第一条,不要过早优化。而且不要过早的开始重写全部代码。 须要注意到这些装箱的耗费,只有在经过工具找到而且定位到最主要问题所在再开始修改代码。
字符串操做是引发内存分配的最大元凶之一,一般在PerfView中占到前五致使内存分配的缘由。应用程序使用字符串来进行序列化,表示JSON和REST。在不支持枚举类型的状况下,字符串能够用来与其余系统进行交互。当咱们定位到是因为string操做致使对性能产生严重影响的时候,须要留意string类的Format(),Concat(),Split(),Join(),Substring()等这些方法。使用StringBuilder可以避免在拼接多个字符串时建立多个新字符串的开销,可是StringBuilder的建立也须要进行良好的控制以免可能会产生的性能瓶颈。
在C#编译器中有以下方法来输出方法前面的xml格式的注释。
public void WriteFormattedDocComment(string text) { string[] lines = text.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None); int numLines = lines.Length; bool skipSpace = true; if (lines[0].TrimStart().StartsWith("///")) { for (int i = 0; i < numLines; i++) { string trimmed = lines[i].TrimStart(); if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3])) { skipSpace = false; break; } } int substringStart = skipSpace ? 4 : 3; for (int i = 0; i < numLines; i++) Console.WriteLine(lines[i].TrimStart().Substring(substringStart)); } else { /* ... */ } }
能够看到,在这片代码中包含有不少字符串操做。代码中使用类库方法来将行分割为字符串,来去除空格,来检查参数text是不是XML文档格式的注释,而后从行中取出字符串处理。
在WriteFormattedDocComment方法每次被调用时,第一行代码调用Split()就会分配三个元素的字符串数组。编译器也须要产生代码来分配这个数组。由于编译器并不知道,若是Splite()存储了这一数组,那么其余部分的代码有可能会改变这个数组,这样就会影响到后面对WriteFormattedDocComment方法的调用。每次调用Splite()方法也会为参数text分配一个string,而后在分配其余内存来执行splite操做。
WriteFormattedDocComment方法中调用了三次TrimStart()方法,在内存环中调用了两次,这些都是重复的工做和内存分配。更糟糕的是,TrimStart()的无参重载方法的签名以下:
namespace System { public class String { public string TrimStart(params char[] trimChars); } }
该方法签名意味着,每次对TrimStart()的调用都回分配一个空的数组以及返回一个string类型的结果。
最后,调用了一次Substring()方法,这个方法一般会致使在内存中分配新的字符串。
解决方法:
和前面的只须要小小的修改便可解决内存分配的问题不一样。在这个例子中,咱们须要从头看,查看问题而后采用不一样的方法解决。好比,能够意识到WriteFormattedDocComment()方法的参数是一个字符串,它包含了方法中须要的全部信息,所以,代码只须要作更多的index操做,而不是分配那么多小的string片断。
下面的方法并无彻底解,可是能够看到如何使用相似的技巧来解决本例中存在的问题。C#编译器使用以下的方式来消除全部的额外内存分配。
private int IndexOfFirstNonWhiteSpaceChar(string text, int start) { while (start < text.Length && char.IsWhiteSpace(text[start])) start++; return start; } private bool TrimmedStringStartsWith(string text, int start, string prefix) { start = IndexOfFirstNonWhiteSpaceChar(text, start); int len = text.Length - start; if (len < prefix.Length) return false; for (int i = 0; i < len; i++) { if (prefix[i] != text[start + i]) return false; } return true; }
WriteFormattedDocComment() 方法的第一个版本分配了一个数组,几个子字符串,一个trim后的子字符串,以及一个空的params数组。也检查了”///”。修改后的代码仅使用了index操做,没有任何额外的内存分配。它查找第一个非空格的字符串,而后逐个字符串比较来查看是否以”///”开头。和使用TrimStart()不一样,修改后的代码使用IndexOfFirstNonWhiteSpaceChar方法来返回第一个非空格的开始位置,经过使用这种方法,能够移除WriteFormattedDocComment()方法中的全部额外内存分配。
本例中使用StringBuilder。下面的函数用来产生泛型类型的全名:
public class Example { // Constructs a name like "SomeType<T1, T2, T3>" public string GenerateFullTypeName(string name, int arity) { StringBuilder sb = new StringBuilder(); sb.Append(name); if (arity != 0) { sb.Append("<"); for (int i = 1; i < arity; i++) { sb.Append("T"); sb.Append(i.ToString()); sb.Append(", "); } sb.Append("T"); sb.Append(i.ToString()); sb.Append(">"); } return sb.ToString(); } }
注意力集中到StringBuilder实例的建立上来。代码中调用sb.ToString()会致使一次内存分配。在StringBuilder中的内部实现也会致使内部内存分配,可是咱们若是想要获取到string类型的结果化,这些分配没法避免。
解决方法:
要解决StringBuilder对象的分配就使用缓存。即便缓存一个可能被随时丢弃的单个实例对象也可以显著的提升程序性能。下面是该函数的新的实现。除了下面两行代码,其余代码均相同
// Constructs a name like "Foo<T1, T2, T3>" public string GenerateFullTypeName(string name, int arity) { StringBuilder sb = AcquireBuilder(); /* Use sb as before */ return GetStringAndReleaseBuilder(sb); }
关键部分在于新的 AcquireBuilder()和GetStringAndReleaseBuilder()方法:
[ThreadStatic] private static StringBuilder cachedStringBuilder; private static StringBuilder AcquireBuilder() { StringBuilder result = cachedStringBuilder; if (result == null) { return new StringBuilder(); } result.Clear(); cachedStringBuilder = null; return result; } private static string GetStringAndReleaseBuilder(StringBuilder sb) { string result = sb.ToString(); cachedStringBuilder = sb; return result; }
上面方法实现中使用了 thread-static字段来缓存StringBuilder对象,这是因为新的编译器使用了多线程的缘由。极可能会忘掉这个ThreadStatic声明。Thread-static字符为每一个执行这部分的代码的线程保留一个惟一的实例。
若是已经有了一个实例,那么AcquireBuilder()方法直接返回该缓存的实例,在清空后,将该字段或者缓存设置为null。不然AcquireBuilder()建立一个新的实例并返回,而后将字段和cache设置为null 。
当咱们对StringBuilder处理完成以后,调用GetStringAndReleaseBuilder()方法便可获取string结果。而后将StringBuilder保存到字段中或者缓存起来,而后返回结果。这段代码极可能重复执行,从而建立多个StringBuilder对象,虽然不多会发生。代码中仅保存最后被释放的那个StringBuilder对象来留做后用。新的编译器中,这种简单的的缓存策略极大地减小了没必要要的内存分配。.NET Framework 和 MSBuild中的部分模块也使用了相似的技术来提高性能。
2,
本文分享了性能优化的一些建议和思考,好比不要过早优化、好工具很重要、性能的关键,在于内存分配等。开发者不要盲目的没有根据的优化,首先定位和查找到形成产生性能问题的缘由点最重要。
使用LINQ 和Lambdas表达式是C#语言强大生产力的一个很好体现,可是若是代码须要执行不少次的时候,可能须要对LINQ或者Lambdas表达式进行重写。
下面的例子使用 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); } }
新的编译器和IDE 体验基于调用FindMatchingSymbol,这个调用很是频繁,在此过程当中,这么简单的一行代码隐藏了基础内存分配开销。为了展现这其中的分配,咱们首先将该单行函数拆分为两行:
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表达式和迭代器,而且没有额外的内存分配开销。这是由于编译器看到symbol 是List<T>类型的集合,由于可以直接将返回的结构性的枚举器绑定到类型正确的本地变量上,从而避免了对struct类型的装箱操做。原先的代码展现了C#语言丰富的表现形式以及.NET Framework 强大的生产力。该着后的代码则更加高效简单,并无添加复杂的代码而增长可维护性。
接下来的例子展现了当咱们试图缓存一部方法返回值时的一个广泛问题:
Visual Studio IDE 的特性在很大程度上创建在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍可以保持Visual Stuido可以响应。下面是获取语法树的第一个版本的代码:
class Parser { /*...*/ public SyntaxTree Syntax { get; } public Task ParseSourceCode() { /*...*/ } } class Compilation { /*...*/ public async Task<SyntaxTree> GetSyntaxTreeAsync() { var parser = new Parser(); // allocation await parser.ParseSourceCode(); // expensive return parser.Syntax; } }
能够看到调用GetSyntaxTreeAsync() 方法会实例化一个Parser对象,解析代码,而后返回一个Task<SyntaxTree>对象。最耗性能的地方在为Parser实例分配内存并解析代码。方法中返回一个Task对象,所以调用者能够await解析工做,而后释放UI线程使得能够响应用户的输入。
因为Visual Studio的一些特性可能须要屡次获取相同的语法树, 因此一般可能会缓存解析结果来节省时间和内存分配,可是下面的代码可能会致使内存分配:
class Compilation { /*...*/ private SyntaxTree cachedResult; public async Task<SyntaxTree> GetSyntaxTreeAsync() { if (this.cachedResult == null) { var parser = new Parser(); // allocation await parser.ParseSourceCode(); // expensive this.cachedResult = parser.Syntax; } return this.cachedResult; } }
代码中有一个SynataxTree类型的名为cachedResult的字段。当该字段为空的时候,GetSyntaxTreeAsync()执行,而后将结果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree对象。问题在于,当有一个类型为Task<SyntaxTree> 类型的async异步方法时,想要返回SyntaxTree的值,编译器会生出代码来分配一个Task来保存执行结果(经过使用Task<SyntaxTree>.FromResult())。Task会标记为完成,而后结果立马返回。分配Task对象来存储执行的结果这个动做调用很是频繁,所以修复该分配问题可以极大提升应用程序响应性。
解决方法:
要移除保存完成了执行任务的分配,能够缓存Task对象来保存完成的结果。
class Compilation { /*...*/ private Task<SyntaxTree> cachedResult; public Task<SyntaxTree> GetSyntaxTreeAsync() { return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync()); } private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync() { var parser = new Parser(); // allocation await parser.ParseSourceCode(); // expensive return parser.Syntax; } }
代码将cachedResult 类型改成了Task<SyntaxTree> 而且引入了async帮助函数来保存原始代码中的GetSyntaxTreeAsync()函数。GetSyntaxTreeAsync函数如今使用 null操做符,来表示当cachedResult不为空时直接返回,为空时GetSyntaxTreeAsync调用GetSyntaxTreeUncachedAsync()而后缓存结果。注意GetSyntaxTreeAsync并无await调用GetSyntaxTreeUncachedAsync。没有使用await意味着当GetSyntaxTreeUncachedAsync返回Task类型时,GetSyntaxTreeAsync 也当即返回Task, 如今缓存的是Task,所以在返回缓存结果的时候没有额外的内存分配。
在大的app或者处理大量数据的App中,还有几点可能会引起潜在的性能问题。
在不少应用程序中,Dictionary用的很广,虽然字很是方便和高校,可是常常会使用不当。在Visual Studio以及新的编译器中,使用性能分析工具发现,许多dictionay只包含有一个元素或者干脆是空的。一个空的Dictionay结构内部会有10个字段在x86机器上的托管堆上会占据48个字节。当须要在作映射或者关联数据结构须要事先常量时间查找的时候,字典很是有用。可是当只有几个元素,使用字典就会浪费大量内存空间。相反,咱们可使用List<KeyValuePair<K,V>>结构来实现便利,对于少许元素来讲,一样高校。若是仅仅使用字典来加载数据,而后读取数据,那么使用一个具备N(log(N))的查找效率的有序数组,在速度上也会很快,固然这些都取决于的元素的个数。
不甚严格的讲,在优化应用程序方面,类和结构提供了一种经典的空间/时间的权衡(trade off)。在x86机器上,每一个类即便没有任何字段,也会分配12 byte的空间 (译注:来保存类型对象指针和同步索引块),可是将类做为方法之间参数传递的时候却十分高效廉价,由于只须要传递指向类型实例的指针便可。结构体若是不撞向的话,不会再托管堆上产生任何内存分配,可是当将一个比较大的结构体做为方法参数或者返回值得时候,须要CPU时间来自动复制和拷贝结构体,而后将结构体的属性缓存到本地便两种以免过多的数据拷贝。
性能优化的一个经常使用技巧是缓存结果。可是若是缓存没有大小上限或者良好的资源释放机制就会致使内存泄漏。在处理大数据量的时候,若是在缓存中缓存了过多数据就会占用大量内存,这样致使的垃圾回收开销就会超过在缓存中查找结果所带来的好处。
在大的系统,或者或者须要处理大量数据的系统中,咱们须要关注产生性能瓶颈症状,这些问题再规模上会影响app的响应性,如装箱操做、字符串操做、LINQ和Lambda表达式、缓存async方法、缓存缺乏大小限制以及良好的资源释放策略、使用Dictionay不当、以及处处传递结构体等。在优化咱们的应用程序的时候,须要时刻注意以前提到过的四点: