Home  >  Article  >  Backend Development  >  Basic essentials of .net program performance

Basic essentials of .net program performance

伊谢尔伦
伊谢尔伦Original
2016-11-24 11:45:201108browse

Bill Chiles (the program manager of the Roslyn compiler) wrote an article "Essential Performance Facts and .NET Framework Tips". The well-known blogger Hanjiang Dudiao excerpted the article and shared some suggestions for performance optimization. and thinking, such as not optimizing prematurely, good tools are important, the key to performance lies in memory allocation, etc., and pointed out that developers should not blindly optimize without basis, and it is most important to first locate and find the cause of performance problems.

Basic essentials of .net program performance

The full text is as follows:

This article provides some performance optimization suggestions, which come from the experience of using managed code to rewrite C# and VB compilers, and uses some real scenarios in writing C# compilers as examples. Demonstrate these optimization experiences. Developing applications for the .NET platform is extremely productive. The powerful and secure programming language and rich class libraries on the .NET platform make application development effective. But with great power comes great responsibility. We should use the powerful capabilities of the .NET framework, but at the same time we need to be prepared to tune our code if we need to process large amounts of data such as files or databases.

 Why the performance optimization lessons from the new compiler also apply to your applications

  Microsoft has rewritten the C# and Visual Basic compilers using managed code and provides a series of new APIs for code modeling and Analysis and development of compilation tools enable Visual Studio to have a richer code-aware programming experience. The experience of rewriting the compiler and developing Visual Studio on the new compiler allowed us to gain very useful performance optimization experience, which can also be used for large-scale .NET applications, or some APPs that need to process large amounts of data. You don't need to know anything about compilers to draw these insights from the C# compiler examples.

 Visual Studio uses the compiler API to implement powerful Intellisense functions, such as code keyword coloring, syntax filling lists, error wavy line prompts, parameter prompts, code problems and modification suggestions, etc. These functions are deep. Popular with developers. When developers enter or modify code, Visual Studio will dynamically compile the code to obtain code analysis and prompts.

When users interact with Apps, they usually want the software to be responsive. The application interface should not block while entering or executing commands. Help or prompts can be displayed quickly or stopped while the user continues typing. Today's apps should avoid blocking the UI thread when performing long-term calculations, causing the user to feel that the program is not smooth enough.

  To learn more about the new compiler, you can visit .NET Compiler Platform ("Roslyn")

 Basic Essentials

 When performance tuning .NET and developing applications with good responsiveness , please consider the following basic tips:

Tip 1: Don’t optimize prematurely

Writing code is more complicated than imagined. The code needs to be maintained, debugged and optimized for performance. An experienced programmer will usually naturally come up with solutions to problems and write efficient code. But sometimes you can fall into the problem of prematurely optimizing your code. For example, sometimes a simple array is enough, but you have to optimize it to use a hash table. Sometimes a simple recalculation is enough, but you have to use a complex cache that may cause memory leaks. When problems are discovered, you should first test for performance issues and then analyze the code.

Tip 2: No evaluation, just guessing

Analysis and measurement don’t lie. The measurement can show whether the CPU is operating at full capacity or if there is disk I/O blocking. The profile will tell you what and how much memory the application allocates and whether the CPU spends a lot of time in garbage collection.

 Performance goals should be set for key user experiences or scenarios, and tests should be written to measure performance. The steps to use scientific methods to analyze the reasons for substandard performance are as follows: use the evaluation report for guidance, hypothesize possible situations, and write experimental code or modify the code to verify our assumptions or corrections. If we set basic performance indicators and test frequently, we can avoid some changes that cause performance regression, thus avoiding us wasting time on unnecessary changes.

 Key point 3: Good tools are important

Good tools allow us to quickly locate the biggest factors affecting performance (CPU, memory, disk) and help us locate the code that generates these bottlenecks. Microsoft has released many performance testing tools such as: Visual Studio Profiler, Windows Phone Analysis Tool, and PerfView.

 PerfView is a free and powerful tool that mainly focuses on some deep-seated issues that affect performance (disk I/O O, GC events, memory), examples of this will be shown later. We can capture performance-related Event Tracing for Windows (ETW) events and view this information at the application, process, stack, and thread scale. PerfView can show how much and what memory is allocated by the application, as well as the contribution of functions and call stacks in the application to memory allocation. For details on these aspects, you can view the very detailed help, Demo and video tutorials about PerfView released with the tool download (such as the video tutorial on Channel9)

Tip 4: Everything is related to memory allocation

You may I think the key to writing responsive .NET-based applications is to use good algorithms, such as using quick sort instead of bubble sort, but this is not the case. The biggest factor in writing a responsive app is memory allocation, especially if the app is very large or handles large amounts of data.

In the practice of developing a responsive IDE using the new compiler API, most of the work is spent on how to avoid memory allocation and manage cache strategies. PerfView traces show that the performance of the new C# and VB compilers is essentially independent of CPU performance bottlenecks. The compiler is reading hundreds, thousands or even tens of thousands of lines of code, reading metadata, and generating compiled code. These operations are actually I/O bound intensive. UI thread latency is almost entirely due to garbage collection. The .NET framework has highly optimized the performance of garbage collection. It can perform most garbage collection operations in parallel when the application code is executed. However, a single memory allocation operation may trigger an expensive garbage collection operation, so that the GC will temporarily suspend all threads for garbage collection (such as Generation 2 type garbage collection)

Common memory allocation and examples

 This part The example though has very little behind it regarding memory allocation. However, if a large application executes enough of these small expressions that result in memory allocations, then these expressions can result in hundreds of megabytes, or even gigabytes, of memory allocations. For example, before the performance testing team locates the problem in the input scenario, a one-minute test that simulates developers writing code in the compiler will allocate several gigabytes of memory.

 Boxing

 Boxing occurs when a value type that is usually allocated on the thread stack or in a data structure, or a temporary value needs to be wrapped into an object (such as allocating an object to store data and returning a pointer) given an Object object). The .NET framework sometimes automatically boxes value types due to the signature of the method or the allocation location of the type. Wrapping a value type into a reference type incurs memory allocation. The .NET framework and language will try to avoid unnecessary boxing, but sometimes boxing operations occur without us noticing. Excessive boxing operations will allocate M on G memory in the application, which means garbage collection will be more frequent and take longer.

To view the boxing operation in PerfView, just enable a trace (trace), and then view the GC Heap Alloc item under the application name (remember, PerfView will report the resource allocation of all processes), if in the allocation phase When you see some value types such as System.Int32 and System.Char, boxing occurs. Selecting a type displays the call stack and the function in which the boxing operation occurred.

 Example 1 string method and its value type parameters

 The following example code demonstrates potentially unnecessary boxing and frequent boxing operations in large systems.

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);
    }
}

This is a basic log class, so the app will call the Log function very frequently to record logs. This method may be called millons times. The problem is that calling the string.Format method will call its overloaded method that accepts a string type and two Object types:

String.Format Method (String, Object, Object)

This overloaded method requires the .NET Framework to box the int type into the object type Then pass it to the method call. In order to solve this problem, the method is to call the id.ToString() and size.ToString() methods, and then pass them into the string.Format method. Calling the ToString() method will indeed result in the allocation of a string, but in string. No matter what happens inside the Format method, string type allocation will occur.

You might think that this basic call to string.Format is just string concatenation, so you might write code like this:

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

In fact, the above line of code will also cause boxing , because the above statement will be called during compilation:

string.Concat(Object, Object, Object);

This method, .NET Framework must box the character constant to call the Concat method.

Solution:

It is very simple to completely fix this problem. Replace the single quotes above with double quotes, that is, replace the character constants with string constants to avoid boxing, because the string type is already a reference type.

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

Example 2 Boxing of enumeration types

 The following example is caused by the frequent use of enumeration types in new C# and VB compilers, especially when doing search operations in Dictionary. Due to the large amount of memory.

public enum Color { Red, Green, Blue }
public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

The problem is very hidden. PerfView will tell you that enmu.GetHashCode() has a boxing operation due to internal implementation reasons. This method will box the representation of the underlying enumeration type. If you look carefully at PerfView, you will see that See that each call to GetHashCode results in two boxing operations. The compiler inserts it once and the .NET Framework inserts it another time.

Solution:

This boxing operation can be avoided by casting the underlying representation of the enumeration when calling GetHashCode.

((int)color).GetHashCode()

Another boxing operation that often occurs when using enumeration types is enum.HasFlag. Parameters passed to HasFlag must be boxed. In most cases, calling HasFlag repeatedly to test through bit operations is very simple and does not require memory allocation.

  Remember the first basic point, don’t optimize prematurely. And don't start rewriting all your code prematurely. It is necessary to pay attention to the cost of these boxings, and only modify the code after finding and locating the main problem through tools.

 String

 String operations are one of the biggest culprits of memory allocation, and usually account for the top five causes of memory allocation in PerfView. The application uses strings for serialization, representing JSON and REST. Strings can be used to interact with other systems without support for enumerated types. When we locate that string operations have a serious impact on performance, we need to pay attention to the Format(), Concat(), Split(), Join(), Substring() and other methods of the string class. Using StringBuilder can avoid the overhead of creating multiple new strings when splicing multiple strings, but the creation of StringBuilder also needs to be well controlled to avoid possible performance bottlenecks.

  例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 。

 When we finish processing the StringBuilder, call the GetStringAndReleaseBuilder() method to get the string result. Then save the StringBuilder into a field or cache it, and return the result. It is possible for this code to be executed repeatedly, creating multiple StringBuilder objects, although this is rarely the case. Only the last released StringBuilder object is saved in the code for later use. In the new compiler, this simple caching strategy greatly reduces unnecessary memory allocations. Some modules in the .NET Framework and MSBuild also use similar techniques to improve performance.

 A simple caching strategy must follow good cache design because it has a size limit cap. Using cache may require more code than before, and requires more maintenance. We should only adopt a caching strategy after discovering that this is a problem. PerfView has shown that StringBuilder contributes significantly to memory allocation.


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn