Best Practices zur Verbesserung der Leistung in C #

Hallo alle zusammen. Wir haben am Vorabend des Kursbeginns "C # Developer" eine Übersetzung eines weiteren nützlichen Materials vorbereitet . Viel Spaß beim Lesen.




Da ich kürzlich eine Liste mit Best Practices in C # für Criteo erstellt habe, dachte ich, es wäre schön, diese öffentlich zu teilen. Der Zweck dieses Artikels besteht darin, eine unvollständige Liste von Codevorlagen bereitzustellen, die vermieden werden sollten, entweder weil sie fragwürdig sind oder weil sie nur schlecht funktionieren. Die Liste mag etwas zufällig erscheinen, da sie leicht aus dem Kontext gerissen ist, aber alle ihre Elemente wurden irgendwann in unserem Code gefunden und verursachten Probleme in der Produktion. Ich hoffe, dies wird als gute Prävention dienen und Ihre Fehler in Zukunft verhindern.

Beachten Sie auch, dass Criteo-Webdienste auf Hochleistungscode basieren, sodass ineffizienter Code vermieden werden muss. In den meisten Anwendungen ist kein spürbarer Unterschied zum Ersetzen einiger dieser Vorlagen erkennbar.

Und zu guter Letzt wurden einige Punkte (zum Beispiel ConfigureAwait) bereits in vielen Artikeln erörtert, daher werde ich nicht näher darauf eingehen. Ziel ist es, eine kompakte Liste von Punkten zu erstellen, auf die Sie achten müssen, und keine detaillierte technische Beschreibung der einzelnen Punkte zu geben.

Synchrones Warten auf asynchronen Code


Erwarten Sie niemals synchron unfertige Aufgaben. Dies gilt unter anderem für : Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll.

Als Verallgemeinerung: Jede synchrone Beziehung zwischen zwei Pool-Threads kann zu einer Erschöpfung des Pools führen. Die Ursachen dieses Phänomens werden in diesem Artikel beschrieben .

ConfigureAwait


Wenn Ihr Code aus einem Synchronisationskontext aufgerufen werden kann, verwenden Sie ihn ConfigureAwait(false)für jeden Ihrer erwarteten Anrufe.

Bitte beachten Sie, dass dies ConfigureAwait nur bei Verwendung eines Schlüsselworts nützlich ist await.

Der folgende Code ist beispielsweise bedeutungslos:

//  ConfigureAwait        
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();

asynchrone Leere


Niemals benutzenasync void . Die in der async voidMethode ausgelöste Ausnahme wird in den Synchronisationskontext übertragen und führt normalerweise zum Absturz der gesamten Anwendung.

Wenn Sie die Aufgabe in Ihrer Methode nicht zurückgeben können (z. B. weil Sie die Schnittstelle implementieren), verschieben Sie den asynchronen Code in eine andere Methode und rufen Sie ihn auf:

interface IInterface
{
    void DoSomething();
}

class Implementation : IInterface
{
    public void DoSomething()
    {
        //      ,
        //      
        _ = DoSomethingAsync();
    }

    private async Task DoSomethingAsync()
    {
        await Task.Delay(100);
    }
}

Vermeiden Sie nach Möglichkeit Asynchronität


Aus Gewohnheit oder wegen des Muskelgedächtnisses können Sie etwas schreiben wie:

public async Task CallAsync()
{
    var client = new Client();
    return await client.GetAsync();
}

Obwohl der Code semantisch korrekt ist, ist die Verwendung eines Schlüsselworts asynchier nicht erforderlich und kann in einer stark belasteten Umgebung zu erheblichem Overhead führen. Versuchen Sie es nach Möglichkeit zu vermeiden:

public Task CallAsync()
{
    var client = new Client();
    return _client.GetAsync();
}

Beachten Sie jedoch, dass Sie nicht auf diese Optimierung zurückgreifen können, wenn Ihr Code in Blöcke eingeschlossen ist (z. B. try/catchoder using):

public async Task Correct()
{
    using (var client = new Client())
    {
        return await client.GetAsync();
    }
}

public Task Incorrect()
{
    using (var client = new Client())
    {
        return client.GetAsync();
    }
}

In der falschen Version ( Incorrect()) wird der Client möglicherweise gelöscht, bevor der GetAsyncAufruf abgeschlossen ist , da die Aufgabe innerhalb des using-Blocks nicht durch Warten erwartet wird.

Regionale Vergleiche


Wenn Sie keinen Grund haben, regionale Vergleiche zu verwenden, verwenden Sie immer ordinale Vergleiche . Obwohl dies aufgrund interner Optimierungen für die Datenpräsentationsformulare in den USA keine große Rolle spielt, ist der Vergleich für die Präsentationsformen anderer Regionen um eine Größenordnung langsamer (und unter Linux bis zu zwei Größenordnungen!). Da der String-Vergleich in den meisten Anwendungen häufig vorkommt, steigt der Overhead erheblich.

ConcurrentBag <T.>


Niemals ConcurrentBag<T>ohne Benchmarking verwenden . Diese Sammlung wurde für ganz bestimmte Anwendungsfälle entwickelt (wenn das Element die meiste Zeit von dem Thread, der es in die Warteschlange gestellt hat, aus der Warteschlange ausgeschlossen wird) und unter schwerwiegenden Leistungsproblemen leidet, wenn es für andere Zwecke verwendet wird. Wenn Sie eine Thread-sichere Sammlung benötigen, bevorzugen ConcurrentQueue <T> .

ReaderWriterLock / ReaderWriterLockSlim <T >
Niemals ohne Benchmarking verwenden. ReaderWriterLock<T>/ReaderWriterLockSlim<T>Obwohl die Verwendung dieser Art von spezialisiertem Synchronisationsprimitiv bei der Arbeit mit Lesern und Schreibern verlockend sein kann, sind die Kosten viel höher als bei einfachen Monitor(mit einem Schlüsselwort verwendeten lock). Wenn die Anzahl der Leser, die gleichzeitig den kritischen Abschnitt ausführen, nicht sehr groß ist, reicht die Parallelität nicht aus, um den erhöhten Overhead zu absorbieren, und der Code funktioniert schlechter.

Bevorzugen Sie Lambda-Funktionen anstelle von Methodengruppen


Betrachten Sie den folgenden Code:

public IEnumerable<int> GetItems()
{
    return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Resharper schlägt vor, Code ohne Lambda-Funktion neu zu schreiben, was möglicherweise etwas sauberer aussieht:

public IEnumerable<int> GetItems()
{
    return _list.Where(Filter);
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Leider führt dies zur Zuweisung von dynamischem Speicher für jeden Anruf. Tatsächlich wird der Aufruf wie folgt kompiliert:

public IEnumerable<int> GetItems()
{
    return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Dies kann erhebliche Auswirkungen auf die Leistung haben, wenn der Code in einem stark belasteten Abschnitt aufgerufen wird.

Durch die Verwendung von Lambda-Funktionen wird die Compileroptimierung gestartet, bei der der Delegat in einem statischen Feld zwischengespeichert wird, wodurch eine Zuordnung vermieden wird. Dies funktioniert nur, wenn Filterstatisch. Wenn nicht, können Sie den Delegaten selbst zwischenspeichern:

private Predicate<int> _filter;

public Constructor()
{
    _filter = new Predicate<int>(Filter);
}

public IEnumerable<int> GetItems()
{
    return _list.Where(_filter);
}

private bool Filter(int element)
{
    return i % 2 == 0;
}

Aufzählungen in Zeichenfolgen konvertieren


Der Aufruf Enum.ToStringin .netist recht teuer, weil Reflexion verwendet wird , nach innen zu konvertieren, und die virtuelle Methode auf die Struktur provoziert Aufruf Verpackung. Dies sollte so weit wie möglich vermieden werden.

Aufzählungen können oft durch konstante Zeichenfolgen ersetzt werden:

//       Numbers.One, Numbers.Two, ...
public enum Numbers
{
    One,
    Two,
    Three
}

public static class Numbers
{
    public const string One = "One";
    public const string Two = "Two";
    public const string Three = "Three";
}

Wenn Sie wirklich eine Aufzählung verwenden müssen, sollten Sie den konvertierten Wert in einem Wörterbuch zwischenspeichern, um den Overhead zu amortisieren.

Listenvergleich


Hinweis: Dies ist in .net Core nicht mehr relevant, da die Optimierung ab Version 2.1 automatisch von JIT durchgeführt wird.

Wenn Sie Aufzählungen als Flags verwenden, kann es verlockend sein, die folgende Methode zu verwenden Enum.HasFlag:

[Flags]
public enum Options
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4
}

private Options _option;

public bool IsOption2Enabled()
{
    return _option.HasFlag(Options.Option2);
}

Dieser Code provoziert zwei Pakete mit Zuordnung: eines für die Konvertierung Options.Option2in Enumund das andere für einen virtuellen Aufruf HasFlagder Struktur. Dies macht diesen Code unverhältnismäßig teuer. Stattdessen sollten Sie die Lesbarkeit opfern und binäre Operatoren verwenden:

public bool IsOption2Enabled()
{
    return (_option & Options.Option2) == Options.Option2;
}

Implementierung von Vergleichsmethoden für Strukturen


Wenn Sie eine Struktur in Vergleichen verwenden (z. B. als Schlüssel für ein Wörterbuch), müssen Sie die Methoden überschreiben Equals/GetHashCode. Die Standardimplementierung verwendet Reflection und ist sehr langsam. Die von Resharper generierte Implementierung ist normalerweise ziemlich gut.

Weitere Informationen hierzu finden Sie hier: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-c

Vermeiden Sie ungeeignete Verpackungen, wenn Sie Strukturen mit Schnittstellen verwenden


Betrachten Sie den folgenden Code:

public class IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue(IValue value)
{
    // ...
}

public void LogValue(IValue value)
{
    // ...
}

Machen IntValuestrukturiert kann verlockend sein , dynamische Zuweisung von Speicher zu vermeiden. Da AddValuesie jedoch SendValueeine Schnittstelle erwarten und die Schnittstellen eine Referenzsemantik aufweisen, wird der Wert bei jedem Aufruf gepackt, wodurch die Vorteile dieser "Optimierung" zunichte gemacht werden. Tatsächlich gibt es sogar noch mehr Speicherzuweisungen als bei IntValueeiner Klasse, da der Wert für jeden Aufruf unabhängig gepackt wird.

Wenn Sie eine API schreiben und erwarten, dass einige Werte Strukturen sind, verwenden Sie generische Methoden:

public struct IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue<T>(T value) where T : IValue
{
    // ...
}

public void LogValue<T>(T value) where T : IValue
{
    // ...
}

Obwohl die Konvertierung dieser Methoden in Universal auf den ersten Blick nutzlos erscheint, können Sie in dem Fall, in dem es sich IntValueum eine Struktur handelt , das Verpacken mit Zuordnung vermeiden .

CancellationToken-Abonnements immer inline


Wenn Sie kündigen CancellationTokenSource, werden alle Abonnements im aktuellen Thread ausgeführt. Dies kann zu ungeplanten Pausen oder sogar zu impliziten Deadlocks führen.

var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel(); //     5 

Sie können sich diesem Verhalten nicht entziehen. CancellationTokenSourceFragen Sie sich daher beim Abbrechen , ob Sie die Erfassung Ihres aktuellen Threads sicher zulassen können. Wenn die Antwort nein ist, wickeln Sie den Anruf Cancelinnerhalb Task.Runes in dem Thread - Pool auszuführen.

TaskCompletionSource-Fortsetzungen werden häufig inline ausgeführt


Wie bei Abonnements CancellationTokensind Fortsetzungen TaskCompletionSourcehäufig inline. Dies ist eine gute Optimierung, kann jedoch implizite Fehler verursachen. Betrachten Sie beispielsweise das folgende Programm:

class Program
{
    private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
    
    public static async Task Deadlock()
    {
        await ProcessAsync();
        _mutex.Wait();
    }
    
    private static Task ProcessAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        
        Task.Run(() =>
        {
            Thread.Sleep(2000); //  - 
            tcs.SetResult(true);
            _mutex.Set();
        });
        
        return tcs.Task;
    }
    
    static void Main(string[] args)
    {
        Deadlock().Wait();
        Console.WriteLine("Will never get there");
    }
}

Der Aufruf tcs.SetResultbewirkt , dass die Fortsetzung await ProcessAsync()im aktuellen Thread ausgeführt wird. Daher wird die Anweisung _mutex.Wait()von demselben Thread ausgeführt, den sie aufrufen soll _mutex.Set(), was zu einem Deadlock führt. Dies kann durch Übergabe des Parameters TaskCreationsOptions.RunContinuationsAsynchronouslyc vermieden werden TaskCompletionSource.

Wenn Sie keinen guten Grund haben, dies zu vernachlässigen, verwenden Sie TaskCreationsOptions.RunContinuationsAsynchronouslybeim Erstellen immer die Option TaskCompletionSource.

Seien Sie vorsichtig: Der Code wird auch kompiliert, wenn Sie TaskContinuationOptions.RunContinuationsAsynchronouslystattdessen verwenden TaskCreationOptions.RunContinuationsAsynchronously, aber die Parameter werden ignoriert und die Fortsetzungen bleiben weiterhin inline. Dies ist ein überraschend häufiger Fehler, da er TaskContinuationOptionsder TaskCreationOptionsautomatischen Vervollständigung vorausgeht .

Task.Run / Task.Factory.StartNew


Wenn Sie keinen Grund zur Verwendung haben Task.Factory.StartNew, wählen Sie immer Task.Runeine Hintergrundaufgabe aus. Task.RunVerwendet sicherere Standardwerte und entpackt vor allem automatisch die zurückgegebene Aufgabe, wodurch nicht offensichtliche Fehler mit asynchronen Methoden verhindert werden können. Betrachten Sie das folgende Programm:

class Program
{
    public static async Task ProcessAsync()
    {
        await Task.Delay(2000);
        Console.WriteLine("Processing done");
    }
    
    static async Task Main(string[] args)
    {
        await Task.Factory.StartNew(ProcessAsync);
        Console.WriteLine("End of program");
        Console.ReadLine();
    }
}

Trotz seines Erscheinungsbilds wird das Programmende früher als die Verarbeitung angezeigt. Dies liegt daran, dass es zurückgegeben Task.Factory.StartNewwird Task<Task>und der Code nur eine externe Aufgabe erwartet. Der richtige Code könnte entweder await Task.Factory.StartNew(ProcessAsync).Unwrap()oder sein await Task.Run(ProcessAsync).

Es gibt nur drei gültige Anwendungsfälle Task.Factory.StartNew:

  • Ausführen einer Aufgabe in einem anderen Scheduler.
  • Ausführen einer Aufgabe in einem dedizierten Thread (mit TaskCreationOptions.LongRunning).
  • ( TaskCreationOptions.PreferFairness).



.



All Articles