.NET程序性能优化基本要领

想了解更多关于新的编译器的信息,可以访问     .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方法和其值类型参数

下面的示例代码演示了潜在的不必要的装箱以及在大的系统中的频繁的装箱操作。

[js] view plaincopyprint?

  1. public class Logger
  2. {
  3. public static void WriteLine(string s)
  4. {
  5. /*...*/
  6. }
  7. }
  8. public class BoxingExample
  9. {
  10. public void Log(int id, int size)
  11. {
  12. var s = string.Format("{0}:{1}", id, size);
  13. Logger.WriteLine(s);
  14. }
  15. }
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类型的方法:

[js] view plaincopyprint?

  1. String.Format Method (String, Object, Object)
String.Format Method (String, Object, Object)

该重载方法要求.NET Framework 把int型装箱为object类型然后将它传到方法调用中去。为了解决这一问题,方法就是调用id.ToString()size.ToString()方法,然后传入到string.Format 方法中去,调用ToString()方法的确会导致一个string的分配,但是在string.Format方法内部不论怎样都会产生string类型的分配。

你可能会认为这个基本的调用string.Format 仅仅是字符串的拼接,所以你可能会写出这样的代码:

[js] view plaincopyprint?

  1. var s = id.ToString() + ‘:‘ + size.ToString();
var s = id.ToString() + ‘:‘ + size.ToString();

实际上,上面这行代码也会导致装箱,因为上面的语句在编译的时候会调用:

[js] view plaincopyprint?

  1. string.Concat(Object, Object, Object);
string.Concat(Object, Object, Object);

这个方法,.NET Framework 必须对字符常量进行装箱来调用Concat方法。

解决方法:

完全修复这个问题很简单,将上面的单引号替换为双引号即将字符常量换为字符串常量就可以避免装箱,因为string类型的已经是引用类型了。

[js] view plaincopyprint?

  1. var s = id.ToString() + ":" + size.ToString();
var s = id.ToString() + ":" + size.ToString();

例2 枚举类型的装箱

下面的这个例子是导致新的C# 和VB编译器由于频繁的使用枚举类型,特别是在Dictionary中做查找操作时分配了大量内存的原因。

[js] view plaincopyprint?

  1. public enum Color { Red, Green, Blue }
  2. public class BoxingExample
  3. {
  4. private string name;
  5. private Color color;
  6. public override int GetHashCode()
  7. {
  8. return name.GetHashCode() ^ color.GetHashCode();
  9. }
  10. }
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的时候将枚举的底层表现形式进行强制类型转换就可以避免这一装箱操作。

[js] view plaincopyprint?

  1. ((int)color).GetHashCode()
((int)color).GetHashCode()

另一个使用枚举类型经常产生装箱的操作时enum.HasFlag。传给HasFlag的参数必须进行装箱,在大多数情况下,反复调用HasFlag通过位运算测试非常简单和不需要分配内存。

要牢记基本要领第一条,不要过早优化。并且不要过早的开始重写所有代码。 需要注意到这些装箱的耗费,只有在通过工具找到并且定位到最主要问题所在再开始修改代码。

字符串

字符串操作是引起内存分配的最大元凶之一,通常在PerfView中占到前五导致内存分配的原因。应用程序使用字符串来进行序列化,表示JSON和REST。在不支持枚举类型的情况下,字符串可以用来与其他系统进行交互。当我们定位到是由于string操作导致对性能产生严重影响的时候,需要留意string类的Format(),Concat(),Split(),Join(),Substring()等这些方法。使用StringBuilder能够避免在拼接多个字符串时创建多个新字符串的开销,但是StringBuilder的创建也需要进行良好的控制以避免可能会产生的性能瓶颈。

例3 字符串操作

在C#编译器中有如下方法来输出方法前面的xml格式的注释。

[js] view plaincopyprint?

  1. public void WriteFormattedDocComment(string text)
  2. {
  3. string[] lines = text.Split(new[] {"\r\n", "\r", "\n"},
  4. StringSplitOptions.None);
  5. int numLines = lines.Length;
  6. bool skipSpace = true;
  7. if (lines[0].TrimStart().StartsWith("///"))
  8. {
  9. for (int i = 0; i < numLines; i++)
  10. {
  11. string trimmed = lines[i].TrimStart();
  12. if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3]))
  13. {
  14. skipSpace = false;
  15. break;
  16. }
  17. }
  18. int substringStart = skipSpace ? 4 : 3;
  19. for (int i = 0; i < numLines; i++)
  20. Console.WriteLine(lines[i].TrimStart().Substring(substringStart));
  21. }
  22. else
  23. {
  24. /* ... */
  25. }
  26. }
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()的无参重载方法的签名如下:

[js] view plaincopyprint?

  1. namespace System
  2. {
  3. public class String
  4. {
  5. public string TrimStart(params char[] trimChars);
  6. }
  7. }
namespace System
{
    public class String
    {
        public string TrimStart(params char[] trimChars);
    }
}

该方法签名意味着,每次对TrimStart()的调用都回分配一个空的数组以及返回一个string类型的结果。

最后,调用了一次Substring()方法,这个方法通常会导致在内存中分配新的字符串。

解决方法:

和前面的只需要小小的修改即可解决内存分配的问题不同。在这个例子中,我们需要从头看,查看问题然后采用不同的方法解决。比如,可以意识到WriteFormattedDocComment()方法的参数是一个字符串,它包含了方法中需要的所有信息,因此,代码只需要做更多的index操作,而不是分配那么多小的string片段。

下面的方法并没有完全解,但是可以看到如何使用类似的技巧来解决本例中存在的问题。C#编译器使用如下的方式来消除所有的额外内存分配。

[js] view plaincopyprint?

  1. private int IndexOfFirstNonWhiteSpaceChar(string text, int start)
  2. {
  3. while (start < text.Length && char.IsWhiteSpace(text[start]))
  4. start++;
  5. return start;
  6. }
  7. private bool TrimmedStringStartsWith(string text, int start, string prefix)
  8. {
  9. start = IndexOfFirstNonWhiteSpaceChar(text, start);
  10. int len = text.Length - start;
  11. if (len < prefix.Length) return false;
  12. for (int i = 0; i < len; i++)
  13. {
  14. if (prefix[i] != text[start + i])
  15. return false;
  16. }
  17. return true;
  18. }
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。下面的函数用来产生泛型类型的全名:

[js] view plaincopyprint?

  1. public class Example
  2. {
  3. // Constructs a name like "SomeType<T1, T2, T3>"
  4. public string GenerateFullTypeName(string name, int arity)
  5. {
  6. StringBuilder sb = new StringBuilder();
  7. sb.Append(name);
  8. if (arity != 0)
  9. {
  10. sb.Append("<");
  11. for (int i = 1; i < arity; i++)
  12. {
  13. sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
  14. }
  15. sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
  16. }
  17. return sb.ToString();
  18. }
  19. }
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对象的分配就使用缓存。即使缓存一个可能被随时丢弃的单个实例对象也能够显著的提高程序性能。下面是该函数的新的实现。除了下面两行代码,其他代码均相同

[js] view plaincopyprint?

  1. // Constructs a name like "Foo<T1, T2, T3>"
  2. public string GenerateFullTypeName(string name, int arity)
  3. {
  4. StringBuilder sb = AcquireBuilder(); /* Use sb as before */
  5. return GetStringAndReleaseBuilder(sb);
  6. }
// 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()方法:

[js] view plaincopyprint?

  1. [ThreadStatic]
  2. private static StringBuilder cachedStringBuilder;
  3. private static StringBuilder AcquireBuilder()
  4. {
  5. StringBuilder result = cachedStringBuilder;
  6. if (result == null)
  7. {
  8. return new StringBuilder();
  9. }
  10. result.Clear();
  11. cachedStringBuilder = null;
  12. return result;
  13. }
  14. private static string GetStringAndReleaseBuilder(StringBuilder sb)
  15. {
  16. string result = sb.ToString();
  17. cachedStringBuilder = sb;
  18. return result;
  19. }
[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表达式

使用LINQ 和Lambdas表达式是C#语言强大生产力的一个很好体现,但是如果代码需要执行很多次的时候,可能需要对LINQ或者Lambdas表达式进行重写。

例5 Lambdas表达式,List<T>,以及IEnumerable<T>

下面的例子使用     LINQ以及函数式风格的代码来通过编译器模型给定的名称来查找符号。

[js] view plaincopyprint?

  1. class Symbol
  2. {
  3. public string Name { get; private set; } /*...*/
  4. }
  5. class Compiler
  6. {
  7. private List<Symbol> symbols;
  8. public Symbol FindMatchingSymbol(string name)
  9. {
  10. return symbols.FirstOrDefault(s => s.Name == name);
  11. }
  12. }
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,这个调用非常频繁,在此过程中,这么简单的一行代码隐藏了基础内存分配开销。为了展示这其中的分配,我们首先将该单行函数拆分为两行:

[js] view plaincopyprint?

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

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

[js] view plaincopyprint?

  1. // Compiler-generated class to hold environment state for lambda
  2. private class Lambda1Environment
  3. {
  4. public string capturedName;
  5. public bool Evaluate(Symbol s)
  6. {
  7. return s.Name == this.capturedName;
  8. }
  9. }
  10. // Expanded Func<Symbol, bool> predicate = s => s.Name == name;
  11. Lambda1Environment l = new Lambda1Environment()
  12. {
  13. capturedName = name
  14. };
  15. var predicate = new Func<Symbol, bool>(l.Evaluate);
// 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>作为第一个参数,可以将上面的展开为下面的代码:

[js] view plaincopyprint?

  1. // Expanded return symbols.FirstOrDefault(predicate) ...
  2. IEnumerable<Symbol> enumerable = symbols;
  3. IEnumerator<Symbol> enumerator = enumerable.GetEnumerator();
  4. while (enumerator.MoveNext())
  5. {
  6. if (predicate(enumerator.Current))
  7. return enumerator.Current;
  8. }
  9. return default(Symbol);
// 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方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。

[js] view plaincopyprint?

  1. public Symbol FindMatchingSymbol(string name)
  2. {
  3. foreach (Symbol s in symbols)
  4. {
  5. if (s.Name == name)
  6. return s;
  7. }
  8. return null;
  9. }
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 强大的生产力。该着后的代码则更加高效简单,并没有添加复杂的代码而增加可维护性。

Aync异步

接下来的例子展示了当我们试图缓存一部方法返回值时的一个普遍问题:

例6 缓存异步方法

Visual Studio IDE 的特性在很大程度上建立在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍能够保持Visual    Stuido能够响应。下面是获取语法树的第一个版本的代码:

[js] view plaincopyprint?

  1. class Parser
  2. {
  3. /*...*/
  4. public SyntaxTree Syntax
  5. {
  6. get;
  7. }
  8. public Task ParseSourceCode()
  9. {
  10. /*...*/
  11. }
  12. }
  13. class Compilation
  14. {
  15. /*...*/
  16. public async Task<SyntaxTree> GetSyntaxTreeAsync()
  17. {
  18. var parser = new Parser(); // allocation
  19. await parser.ParseSourceCode(); // expensive
  20. return parser.Syntax;
  21. }
  22. }
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的一些特性可能需要多次获取相同的语法树, 所以通常可能会缓存解析结果来节省时间和内存分配,但是下面的代码可能会导致内存分配:

[js] view plaincopyprint?

  1. class Compilation
  2. { /*...*/
  3. private SyntaxTree cachedResult;
  4. public async Task<SyntaxTree> GetSyntaxTreeAsync()
  5. {
  6. if (this.cachedResult == null)
  7. {
  8. var parser = new Parser(); // allocation
  9. await parser.ParseSourceCode(); // expensive
  10. this.cachedResult = parser.Syntax;
  11. }
  12. return this.cachedResult;
  13. }
  14. }
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对象来保存完成的结果。

[js] view plaincopyprint?

  1. class Compilation
  2. { /*...*/
  3. private Task<SyntaxTree> cachedResult;
  4. public Task<SyntaxTree> GetSyntaxTreeAsync()
  5. {
  6. return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync());
  7. }
  8. private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync()
  9. {
  10. var parser = new Parser(); // allocation
  11. await parser.ParseSourceCode(); // expensive
  12. return parser.Syntax;
  13. }
  14. }
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不当、以及到处传递结构体等。在优化我们的应用程序的时候,需要时刻注意之前提到过的四点:

  1. 不要进行过早优化——在定位和发现问题之后再进行调优。
  2. 专业测试不会说谎——没有评测,便是猜测。
  3. 好工具很重要。——下载         PerfView,然后去看使用教程。
  4. 内存分配决定app的响应性。——这也是新的编译器性能团队花的时间最多的地方。

转自:http://www.csdn.net/article/2014-08-27/2821394-.NET-Framework-Tips/2

时间: 2024-10-29 06:07:48

.NET程序性能优化基本要领的相关文章

Java程序性能优化——性能调优层次

为了提升系统性能,开发人员可以从系统的各个角度和层次对系统进行优化.除了最常见的代码优化外,在软件架构上.JVM虚拟机层.数据库以及操作系统层都可以通过各种手段进行调优,从而在整体上提升系统的性能. 设计调优 设计调优处于所有调优手段的上层,它往往需要在软件开发之前进行.在软件开发之初,软件架构师就应该评估系统可能存在的各种潜在的问题,并给出合理的设计方案.由于软件设计和架构对软件整体有决定性的影响,所以,设计调优对系统性能的影响也是最大的.如果说,代码优化.JVM优化都是对系统微观层面上"量&

程序性能优化之SQL篇

如果说功能是程序的躯体,那么性能就是程序的灵魂.完整的功能可以保证程序的躯体是健全的,而良好的性能才是程序灵魂的象征,本文就程序的性能优化做简单的介绍. 最近对程序的性能的体会尤为深刻.最近做了一个数据查询和显示的功能,从7张表大概1500条数据中查询25条数据并且显示出来,时间消耗1秒钟.我的计算机参数为:CPU:i5处理器,4G内存.这个执行速度相当的慢,好在我查询的数据量比较小,等待时间不是很多,但是程序性能优化确实刻不容缓. 仔细分析了程序,发现有很多地方都是需要修改的,我们先从数据库开

Java程序性能优化技巧

多线程.集合.网络编程.内存优化.缓冲..spring.设计模式.软件工程.编程思想 1.生成对象时,合理分配空间和大小new ArrayList(100); 2.优化for循环Vector vect = new Vector(1000);for( inti=0; i<vect.size(); i++){ ...}for循环部分改写成:int size = vect.size();for( int i=0; i>size; i++){ ...} 如果size=1000,就可以减少1000次si

《Java程序性能优化》学习笔记 Ⅰ设计优化

豆瓣读书:http://book.douban.com/subject/19969386/ 第一章 Java性能调优概述 1.性能的参考指标 执行时间: CPU时间: 内存分配: 磁盘吞吐量: 网络吞吐量: 响应时间: 2.木桶定律   系统的最终性能取决于系统中性能表现最差的组件,例如window系统内置的评分就是选取最低分.可能成为系统瓶颈的计算资源如,磁盘I/O,异常,数据库,锁竞争,内存等. 性能优化的几个方面,如设计优化,Java程序优化,并行程序开发及优化,JVM调优,Java性能调

[JAVA] java程序性能优化

一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util.vector; class cel { void method (vector vector) { for (int i = 0; i < vector.size (); i++) // violation ; // ... } } 更正: class cel_fixed { void metho

Java程序性能优化之代理模式

代理模式的用处很多,有的是为了系统安全,有的是为了远程调用,这里我们,主要探讨下由于程序性能优化的延迟加载. 首先我们来看下代理模式设计 先首先简单阐述下什么叫代理模式吧 代理设计模式有一个接口,另外还有真实主题类和代理类,真实类和代理类都实现了接口,代理类和真实主题类是关联和聚合关系.客户端与接口关联. 代理分为静态代理和动代态代理所谓静态代理是为真实主题手动创建一个代理,而动态代理则是jvm在运行时运用字节码加载技术自动创建一个代理,并不用关心接口和真是主题类 具体如何实现 哦,对了差点忘了

Java程序性能优化:代码优化

现在计算机的处理性能越来越好,加上JDK升级对一些代码的优化,在代码层针对一些细节进行调整可能看不到性能的明显提升, 但是我觉得在开发中注意这些,更多的是可以保持一种性能优先的意识,对一些敲代码时间比较短的同学挺有意义的. 一 循环条件下,循环体和判断条件中,都要避免对使用复杂表达式,减少对变量的重复计算 1.在循环中应该避免使用复杂的表达式. 在循环中,循环条件会被反复计算,应该避免把一些计算放在循环进行的部分中,程序将会运行的更快.比如: for(int i=0;i<list.size();

关于程序性能优化基础的一些个人总结

性能点: I/O,系统调用,并发/锁,内存分配,内存拷贝,函数调用消耗,编译优化,算法 I/O性能优化: I/O性能主要耗费点:系统调用,磁盘读写,网络通讯等 优化点:减少系统调用次数,减少磁盘读写次数,减少阻塞等待 优化手段: a. 使用非阻塞模式 b. 使用带缓存的I/O,减少磁盘读写次数 c. I/O多路复用,select/poll/epoll d. 异步I/O 系统调用: 耗费点:用户态和系统态切换时耗 优化点:减少不必要的系统调用 优化手段: a. I/O操作,根据具体情况,使用std

Java程序性能优化——设计优化

原文出自:http://blog.csdn.net/anxpp/article/details/51914119,转载请注明出处,谢谢! 1.前言 OK,之前写了一篇文章:"23种设计模式介绍以及在Java中的应用"详细介绍了如何将设计模式应用到Java编程中,而本文旨在介绍如何利用他们优化我们的程序,使其性能更佳. 设计模式的详细介绍请参照上面链接中的文章,不是本文的重点. 而Java程序的性能优化,不一定就仅仅是以提高系统性能为目的的,还可能是以用户体验.系统可维护性等为目的. 2