Heim  >  Artikel  >  Backend-Entwicklung  >  Grundlegende Grundlagen der .net-Programmleistung

Grundlegende Grundlagen der .net-Programmleistung

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

Bill Chiles (der Programmmanager des Roslyn-Compilers) hat einen Artikel „Wesentliche Leistungsfakten und .NET Framework-Tipps“ geschrieben. Der bekannte Blogger Hanjiang Dudiao hat einige Vorschläge und Gedanken zur Leistungsoptimierung geteilt. B. nicht vorzeitig optimieren, gute Tools sind wichtig, der Schlüssel zur Leistung liegt in der Speicherzuweisung usw. und es wurde darauf hingewiesen, dass Entwickler nicht blind ohne Grundlage optimieren sollten, sondern zunächst die Ursache von Leistungsproblemen lokalisieren und finden sollten.

Grundlegende Grundlagen der .net-Programmleistung

Der vollständige Text lautet wie folgt:

Dieser Artikel enthält einige Vorschläge zur Leistungsoptimierung, die aus den Erfahrungen beim Umschreiben von C# und VB abgeleitet wurden Compiler und verwendet einige reale Szenarien beim Schreiben eines C#-Compilers als Beispiele, um diese Optimierungserfahrungen zu demonstrieren. Die Entwicklung von Anwendungen für die .NET-Plattform ist äußerst produktiv. Die leistungsstarke und sichere Programmiersprache und die umfangreichen Klassenbibliotheken auf der .NET-Plattform machen die Anwendungsentwicklung effektiv. Aber mit großer Macht geht auch große Verantwortung einher. Wir sollten die leistungsstarken Funktionen des .NET-Frameworks nutzen, müssen aber gleichzeitig darauf vorbereitet sein, unseren Code zu optimieren, wenn wir große Datenmengen wie Dateien oder Datenbanken verarbeiten müssen.

Warum die Erkenntnisse aus der Leistungsoptimierung des neuen Compilers auch für Ihre Anwendungen gelten

Microsoft hat die C#- und Visual Basic-Compiler so umgeschrieben, dass sie verwalteten Code verwenden, und stellt eine Reihe neuer APIs zur Verfügung, die zum Ausführen von Code verwendet werden Modellierung und Analyse sowie die Entwicklung von Kompilierungstools, die Visual Studio ein umfassenderes Code-bewusstes Programmiererlebnis bieten. Durch die Erfahrung beim Umschreiben des Compilers und beim Entwickeln von Visual Studio auf dem neuen Compiler konnten wir sehr nützliche Erfahrungen bei der Leistungsoptimierung sammeln, die auch für große .NET-Anwendungen oder einige APPs, die große Datenmengen verarbeiten müssen, genutzt werden können. Sie müssen nichts über Compiler wissen, um diese Erkenntnisse aus den C#-Compiler-Beispielen zu gewinnen.

Visual Studio verwendet die API des Compilers, um leistungsstarke Intellisense-Funktionen zu implementieren, z. B. Code-Schlüsselwortfärbung, Syntaxfülllisten, Fehlerwellenlinien-Eingabeaufforderungen, Parameter-Eingabeaufforderungen, Codeprobleme und Änderungsvorschläge usw. Diese Funktionen sind sehr beliebt unter Entwicklern. Wenn Entwickler Code eingeben oder ändern, kompiliert Visual Studio den Code dynamisch, um eine Codeanalyse und Eingabeaufforderungen zu erhalten.

Wenn Benutzer mit der App interagieren, möchten sie normalerweise, dass die Software reagiert. Die Anwendungsschnittstelle sollte beim Eingeben oder Ausführen von Befehlen nicht blockieren. Hilfe oder Eingabeaufforderungen können schnell angezeigt oder gestoppt werden, während der Benutzer weiter tippt. Heutige Apps sollten es vermeiden, den UI-Thread zu blockieren, wenn Langzeitberechnungen durchgeführt werden, sodass der Benutzer das Gefühl hat, dass das Programm nicht flüssig genug ist.

Wenn Sie mehr über den neuen Compiler erfahren möchten, können Sie die .NET Compiler-Plattform („Roslyn“) besuchen.

Grundlegende Grundlagen

Bei der Leistungsoptimierung von .NET Bei der Optimierung Beachten Sie beim Entwickeln von Anwendungen mit guter Reaktionsfähigkeit bitte die folgenden grundlegenden Tipps:

Tipp 1: Optimieren Sie nicht vorzeitig

Das Schreiben von Code ist komplizierter als Sie denken. Code muss gewartet, debuggt und gewartet werden auf Leistung optimiert. Ein erfahrener Programmierer wird in der Regel selbstverständlich Lösungen für Probleme finden und effizienten Code schreiben. Aber manchmal kann es vorkommen, dass Sie auf das Problem stoßen, Ihren Code vorzeitig zu optimieren. Manchmal reicht beispielsweise ein einfaches Array aus, aber Sie müssen es optimieren, um eine Hash-Tabelle zu verwenden. Manchmal reicht eine einfache Neuberechnung aus, aber Sie müssen einen komplexen Cache verwenden, der zu Speicherverlusten führen kann. Wenn Probleme entdeckt werden, sollten Sie zunächst auf Leistungsprobleme testen und dann den Code analysieren.

Tipp 2: Keine Bewertung, nur Spekulation

Analyse und Messung lügen nicht. Die Messung kann zeigen, ob die CPU mit voller Kapazität arbeitet oder ob Festplatten-I/O blockiert ist. Das Profil verrät Ihnen, welchen und wie viel Speicher die Anwendung zuweist und ob die CPU viel Zeit mit der Garbage Collection verbringt.

Für wichtige Benutzererfahrungen oder -szenarien sollten Leistungsziele festgelegt und Tests geschrieben werden, um die Leistung zu messen. Die Schritte zur Verwendung wissenschaftlicher Methoden zur Analyse der Gründe für minderwertige Leistung lauten wie folgt: Verwenden Sie den Bewertungsbericht als Orientierung, stellen Sie Hypothesen über mögliche Situationen auf und schreiben Sie experimentellen Code oder ändern Sie den Code, um unsere Annahmen oder Korrekturen zu überprüfen. Wenn wir grundlegende Leistungsindikatoren festlegen und häufig testen, können wir einige Änderungen vermeiden, die zu Leistungsrückgängen führen, und verhindern so, dass wir Zeit mit unnötigen Änderungen verschwenden.

Tipp 3: Gute Tools sind wichtig

Gute Tools ermöglichen es uns, die größten Faktoren, die die Leistung beeinflussen (CPU, Speicher, Festplatte), schnell zu lokalisieren und diese Engpässe zu lokalisieren. Microsoft hat viele Leistungstesttools veröffentlicht, wie zum Beispiel: Visual Studio Profiler, Windows Phone Analysis Tool und PerfView.

PerfView ist ein kostenloses und leistungsstarkes Tool, das sich hauptsächlich auf einige tiefgreifende Probleme konzentriert, die sich auf die Leistung auswirken (Disk I /O, GC-Ereignisse, Speicher), Beispiele hierfür werden später gezeigt. Wir können leistungsbezogene Event Tracing for Windows (ETW)-Ereignisse erfassen und diese Informationen auf Anwendungs-, Prozess-, Stack- und Thread-Ebene anzeigen. PerfView kann anzeigen, wie viel und welcher Speicher von der Anwendung zugewiesen wird und welchen Beitrag Funktionen und Aufrufstapel in der Anwendung zur Speicherzuweisung leisten. Einzelheiten zu diesen Aspekten finden Sie in der sehr detaillierten Hilfe, Demo und Video-Tutorials zu PerfView, die mit dem Tool-Download veröffentlicht wurden (z. B. das Video-Tutorial auf Channel9)

Tipp 4: Alles hängt mit der Speicherzuweisung zusammen

Sie denken vielleicht, dass der Schlüssel zum Schreiben reaktionsfähiger .NET-basierter Anwendungen in der Verwendung guter Algorithmen liegt, z. B. in der Verwendung einer Schnellsortierung anstelle einer Blasensortierung, aber das ist nicht der Fall. Der größte Faktor beim Schreiben einer responsiven App ist die Speicherzuweisung, insbesondere wenn die App sehr groß ist oder große Datenmengen verarbeitet.

Bei der Entwicklung einer reaktionsfähigen IDE mithilfe der neuen Compiler-API wird der größte Teil der Arbeit darauf verwendet, Speicherzuweisungen zu vermeiden und Cache-Strategien zu verwalten. PerfView-Traces zeigen, dass die Leistung der neuen C#- und VB-Compiler im Wesentlichen unabhängig von CPU-Leistungsengpässen ist. Der Compiler liest Hunderte, Tausende oder sogar Zehntausende Codezeilen, liest Metadaten und generiert kompilierten Code. Diese Vorgänge sind tatsächlich E/A-intensiv. Die Latenz des UI-Threads ist fast ausschließlich auf die Speicherbereinigung zurückzuführen. Das .NET-Framework hat die Leistung der Garbage Collection stark optimiert. Es kann die meisten Garbage Collection-Vorgänge parallel ausführen, wenn der Anwendungscode ausgeführt wird. Ein einzelner Speicherzuweisungsvorgang kann jedoch einen teuren Speicherbereinigungsvorgang auslösen, sodass der GC alle Threads für die Speicherbereinigung (z. B. Speicherbereinigung vom Typ Generation 2) vorübergehend anhält

Gemeinsame Speicherzuweisung und Beispiele

Obwohl hinter den Beispielen in diesem Teil nur sehr wenig über die Speicherzuordnung steckt. Wenn eine große Anwendung jedoch genügend dieser kleinen Ausdrücke ausführt, die zu Speicherzuweisungen führen, können diese Ausdrücke zu Speicherzuweisungen von Hunderten Megabyte oder sogar Gigabyte führen. Bevor das Leistungstestteam beispielsweise das Problem im Eingabeszenario lokalisiert, reserviert ein einminütiger Test, der Entwickler beim Schreiben von Code im Compiler simuliert, mehrere Gigabyte Speicher.

Boxing

Boxing tritt auf, wenn ein Werttyp, der normalerweise auf dem Thread-Stack oder in einer Datenstruktur zugewiesen wird, oder ein temporärer Wert in ein Objekt eingeschlossen werden muss (z. B. beim Zuweisen eines Objekts). um Daten zu speichern und einen Zeiger auf ein Object-Objekt zurückzugeben). Das .NET Framework fasst Werttypen aufgrund der Signatur der Methode oder des Zuordnungsorts des Typs manchmal automatisch in Boxen ein. Das Umschließen eines Werttyps in einen Referenztyp erfordert eine Speicherzuweisung. Das .NET-Framework und die .NET-Sprache versuchen, unnötiges Boxen zu vermeiden, aber manchmal passieren Boxing-Vorgänge, ohne dass wir es bemerken. Übermäßige Boxing-Vorgänge belegen M-auf-G-Speicher in der Anwendung, was bedeutet, dass die Speicherbereinigung häufiger erfolgt und länger dauert.

Um den Boxing-Vorgang in PerfView anzuzeigen, aktivieren Sie einfach eine Ablaufverfolgung (Trace) und zeigen Sie dann das GC-Heap-Alloc-Element unter dem Anwendungsnamen an (denken Sie daran, dass PerfView die Ressourcenzuteilung aller Prozesse meldet), sofern in der Zuordnung vorhanden Wenn Sie einige Werttypen wie System.Int32 und System.Char sehen, ist ein Boxing aufgetreten. Wenn Sie einen Typ auswählen, werden der Aufrufstapel und die Funktion angezeigt, in der der Boxvorgang stattgefunden hat.

Beispiel 1 String-Methode und ihre Werttypparameter

Der folgende Beispielcode demonstriert potenziell unnötiges Boxen und häufige Boxing-Vorgänge in großen Systemen.

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

Dies ist eine grundlegende Protokollklasse, daher ruft die App die Protokollfunktion sehr häufig auf, um Protokolle aufzuzeichnen. Diese Methode kann millionenfach aufgerufen werden. Das Problem besteht darin, dass der Aufruf der string.Format-Methode ihre überladene Methode aufruft, die einen String-Typ und zwei Objekttypen akzeptiert:

String.Format Method (String, Object, Object)

Dies Für eine überladene Methode muss das .NET Framework den int-Typ in einen Objekttyp packen und an den Methodenaufruf übergeben. Um dieses Problem zu lösen, besteht die Methode darin, die Methoden id.ToString() und size.ToString() aufzurufen und sie dann an die Methode string.Format zu übergeben. Der Aufruf der Methode ToString() führt tatsächlich zur Zuordnung eine Zeichenfolge, aber in Zeichenfolge. Unabhängig davon, was innerhalb der Format-Methode geschieht, erfolgt die Zuordnung des Zeichenfolgentyps.

Sie könnten denken, dass dieser grundlegende Aufruf string.Format nur eine Zeichenfolgenverkettung ist, also könnten Sie Code wie diesen schreiben:

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

Tatsächlich verursacht die obige Codezeile auch Boxing, da die obige Anweisung während der Kompilierung aufgerufen wird:

string.Concat(Object, Object, Object);

Für diese Methode gilt: .NET Framework muss die Zeichenkonstante einschließen, um die Concat-Methode aufzurufen.

Lösung:

Es ist sehr einfach, dieses Problem vollständig durch doppelte Anführungszeichen zu beheben, d. h. die Zeichenkonstanten durch Zeichenfolgenkonstanten zu ersetzen, um Boxen zu vermeiden. weil string Der Typ ist bereits ein Referenztyp.

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

Beispiel 2 Boxen von Aufzählungstypen

Das folgende Beispiel ist das Ergebnis des neuen C# The Der VB-Compiler reserviert aufgrund der häufigen Verwendung von Aufzählungstypen, insbesondere bei der Durchführung von Suchvorgängen im Wörterbuch, viel Speicher.

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

Das Problem ist sehr versteckt. PerfView teilt Ihnen mit, dass enmu.GetHashCode() aus internen Implementierungsgründen eine Boxing-Operation hat. Box: Wenn Sie sich PerfView genau ansehen, werden Sie feststellen, dass jeder Aufruf von GetHashCode zwei Boxing-Operationen erzeugt. Der Compiler fügt es einmal ein und das .NET Framework fügt es ein anderes Mal ein.

Lösung:

Diese Boxing-Operation kann vermieden werden, indem beim Aufruf von GetHashCode die zugrunde liegende Darstellung der Aufzählung umgewandelt wird.

((int)color).GetHashCode()

Eine weitere Boxing-Operation, die häufig bei der Verwendung von Aufzählungstypen auftritt, ist enum.HasFlag. An HasFlag übergebene Parameter müssen in Boxen eingefügt werden. In den meisten Fällen ist der wiederholte Aufruf von HasFlag zum Testen durch Bitoperationen sehr einfach und erfordert keine Speicherzuweisung.

Denken Sie an den ersten Grundpunkt und optimieren Sie nicht voreilig. Und beginnen Sie nicht vorzeitig, Ihren gesamten Code neu zu schreiben. Es ist notwendig, auf die Kosten dieser Boxen zu achten und den Code erst zu ändern, nachdem das Hauptproblem mithilfe von Tools gefunden und lokalisiert wurde.

String

String-Operationen sind eine der größten Ursachen für die Speicherzuweisung und gehören normalerweise zu den fünf häufigsten Ursachen für die Speicherzuweisung in PerfView. Die Anwendung verwendet Zeichenfolgen zur Serialisierung, die JSON und REST darstellen. Strings können zur Interaktion mit anderen Systemen verwendet werden, ohne dass Aufzählungstypen unterstützt werden. Wenn wir feststellen, dass String-Operationen einen gravierenden Einfluss auf die Leistung haben, müssen wir auf Format(), Concat(), Split(), Join(), Substring() und andere Methoden der String-Klasse achten. Durch die Verwendung von StringBuilder kann der Aufwand für die Erstellung mehrerer neuer Zeichenfolgen beim Zusammenfügen mehrerer Zeichenfolgen vermieden werden. Die Erstellung von StringBuilder muss jedoch auch gut kontrolliert werden, um mögliche Leistungsengpässe zu vermeiden.

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

Wenn wir mit der Verarbeitung des StringBuilder fertig sind, rufen Sie die Methode GetStringAndReleaseBuilder() auf, um das String-Ergebnis zu erhalten. Speichern Sie dann den StringBuilder in einem Feld oder zwischenspeichern Sie ihn und geben Sie das Ergebnis zurück. Es ist möglich, dass dieser Code wiederholt ausgeführt wird und mehrere StringBuilder-Objekte erstellt werden, obwohl dies selten der Fall ist. Nur das zuletzt freigegebene StringBuilder-Objekt wird zur späteren Verwendung im Code gespeichert. Im neuen Compiler reduziert diese einfache Caching-Strategie die unnötige Speicherzuweisung erheblich. Einige Module in .NET Framework und MSBuild verwenden auch ähnliche Techniken zur Leistungsverbesserung.

Eine einfache Caching-Strategie muss einem guten Caching-Design folgen, da es eine Größenbeschränkung gibt. Die Verwendung des Caches erfordert möglicherweise mehr Code als zuvor und erfordert mehr Wartung. Wir sollten eine Caching-Strategie erst anwenden, nachdem wir festgestellt haben, dass dies ein Problem darstellt. PerfView hat gezeigt, dass StringBuilder erheblich zur Speicherzuweisung beiträgt.


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn