首页 > 代码库 > .NET程序的性能要领和优化建议
.NET程序的性能要领和优化建议
前几天在老赵的博客上看到。Bill Chiles (Roslyn 编译器的Program Manager)写了一篇文章叫做《Essential Performance Facts and .NET Framework Tips》。这篇文章是一个14页的pdf,当时我是在地铁上在Lumia手机上看的。觉得非常是不错,这里也建议大家直接下载阅读原文。我这里试着翻译一下。以加深自己印象,后面也有一些思考。以下是原文内容:
---------------------------------------------------------------------------
本文提供了一些性能优化的建议,这些经验来自于使用托管代码重写C# 和 VB编译器,并以编写C# 编译器中的一些真实场景作为样例来展示这些优化经验。.NET 平台开发应用程序具有极高的生产力。.NET 平台上强大安全的编程语言以及丰富的类库,使得开发应用变得卓有成效。可是能力越大责任越大。我们应该使用.NET框架的强大能力。但同一时候假设我们须要处 理大量的数据比方文件或者数据库也须要准备对我们的代码进行调优。
为什么来自新的编译器的性能优化经验也适用于您的应用程序
微软使用托管代码重写了C#和Visual Basic的编译器。并提供了一些列新的API来进行代码建模和分析、开发编译工具,使得Visual Studio具有更加丰富的代码感知的编程体验。
重写编译器,而且在新的编译器上开发Visual Studio的经验使得我们获得了非常实用的性能优化经验。这些经验也能用于大型的.NET应用,或者一些须要处理大量数据的APP上。你不须要了解编译 器。也能够从C#编译器的样例中得出这些见解。
Visual Studio使用了编译器的API来实现了强大的智能感知(Intellisense)功能,如代码keyword着色,语法填充列表。错误波浪线提示,參数提 示。代码问题及改动建议等,这些功能深受开发人员欢迎。
Visual Studio在开发人员输入或者改动代码的时候。会动态的编译代码来获得对代码的分析和提示。
当用户和App进行交互的时候,通常希望软件具有好的响应性。输入或者运行命令的时候,应用程序界面不应该被堵塞。帮助或者提示能够迅速显示出来或者当用户继续输入的时候停止提示。如今的App应该避免在运行长时间计算的时候堵塞UI线程从而让用户感觉程序不够流畅。
想了解很多其它关于新的编译器的信息,能够訪问 .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型的垃圾回收)
常见的内存分配以及样例
这部分的样例尽管背后关于内存分配的地方非常少。可是,假设一个大的应用程序运行足够多的这些小的会导致内存分配的表达式,那么这些表达式会导致几百 M,甚至几G的内存分配。比方,在性能測试团队把问题定位到输入场景之前,一分钟的測试模拟开发人员在编译器里面编写代码会分配几G的内存。
装箱
装箱发 生在当通常分配在线程栈上或者数据结构中的值类型,或者暂时的值须要被包装到对象中的时候(比方分配一个对象来存放数据,活着返回一个指针给一个 Object对象)。.NET框架由于方法的签名或者类型的分配位置,有些时候会自己主动对值类型进行装箱。将值类型包装为引用类型会产生内存分配。
.NET 框架及语言会尽量避免不必要的装箱,可是有时候在我们没有注意到的时候会产生装箱操作。过多的装箱操作会在应用程序中分配成M上G的内存,这就意味着垃圾 回收的更加频繁,也会花更长时间。
在PerfView中查看装箱操作。仅仅须要开启一个追踪(trace),然后查看应用程序名字以下的GC Heap Alloc 项(记住,PerfView会报告全部的进程的资源分配情况)。假设在分配相中看到了一些诸如System.Int32和System.Char的值类 型,那么就发生了装箱。选择一个类型,就会显示调用栈以及发生装箱的操作的函数。
例1 string方法和其值类型參数
以下的演示样例代码演示了潜在的不必要的装箱以及在大的系统中的频繁的装箱操作。
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方法。
解决方法:
全然修复这个问题非常easy,将上面的单引號替换为双引號即将字符常量换为字符串常量就能够避免装箱,由于string类型的已经是引用类型了。
var s = id.ToString() + ":" + size.ToString();
例2 枚举类型的装箱
以下的这个样例是导致新的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通过位运算測试非常easy和不须要分配内存。
要牢记基本要领第一条,不要过早优化。
而且不要过早的開始重写全部代码。
须要注意到这些装箱的耗费。仅仅有在通过工具找到而且定位到最主要问题所在再開始改动代码。
字符串
字符串操作是引起内存分配的最大元凶之中的一个,通常在PerfView中占到前五导致内存分配的原因。应用程序使用字符串来进行序列化,表示JSON和 REST。在不支持枚举类型的情况下,字符串能够用来与其它系统进行交互。当我们定位到是由于string操作导致对性能产生严重影响的时候,须要留意 string类的Format(),Concat(),Split(),Join(),Substring()等这些方法。
使用StringBuilder能够避免在拼接多个字符串时创建多个新字符串的开销,可是StringBuilder的创建也须要进行良好的控制以避免可能会产生的性能瓶颈。
例3 字符串操作
在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()方法中的全部额外内存分配。
例4 StringBuilder
本例中使用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 中的部分模块也使用了相似的技术来提升性能。
简单的缓存策略必须遵循良好的缓存设计。由于他有大小的限制cap。使用缓存可能比之前有很多其它的代码。也须要很多其它的维护工作。我们仅仅有在发现这是个问题之后才应该採缓存策略。PerfView已经显示出StringBuilder对内存的分配贡献相当大。
LINQ和Lambdas表达式
使用LINQ 和Lambdas表达式是C#语言强大生产力的一个非常好体现,可是假设代码须要运行非常多次的时候。可能须要对LINQ或者Lambdas表达式进行重写。
例5 Lambdas表达式,List<T>,以及IEnumerable<T>
以下的样例使用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方法,将单个语句使用六行代码替代。这些代码依然连贯。易于阅读和理解。也非常easy实现。
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 强大的生产力。该着后的代码则更加高效简单,并没有加入复杂的代码而添加可维护性。
Aync异步
接下来的样例展示了当我们试图缓存一部方法返回值时的一个普遍问题:
例6 缓存异步方法
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不当、以及到处传递结构体等。在优化我们的应用程序的时候。须要时刻注意之前提到过的四点:
- 不要进行过早优化——在定位和发现问题之后再进行调优。
- 专业測试不会说谎——没有评測,便是推測。
- 好工具非常重要。——下载PerfView,然后去看使用教程。
- 内存分配决定app的响应性。——这也是新的编译器性能团队花的时间最多的地方。
參考资料
- 假设想观看关于这一话题的演讲,能够在Channel 9 上观看。
- VS Profiler基础 http://msdn.microsoft.com/en-us/library/ms182372.aspx
- .NET 英语程序性能分析工具一览 http://msdn.microsoft.com/en-us/library/hh156536.aspx
- Windows Phone性能分析工具http://msdn.microsoft.com/en-us/magazine/hh781024.aspx
- 一些C# 和VB性能优化建议 http://msdn.microsoft.com/en-us/library/ms173196(v=vs.110).aspx(注:原文中该链接无内容,连接地址应该使http://msdn.microsoft.com/en-us/library/ms173196(v=vs.100).aspx )
- 一些高级优化建议 http://curah.microsoft.com/4604/improving-your-net-apps-startup-performance
.NET程序的性能要领和优化建议