Performancefalle Linq

6. Juli 2011

LINQ macht viele Dinge im C#-Code einfacher, ausdrucksvoller und eleganter. Aber manchmal bietet es doch Stolpersteine, die eine Fehlersuche oder das Suchen von schlecht performanten Code stark verkomplizieren.

Häufig sind die Gründe dafür zwar generell bekannt, werden aber nicht ausreichend bei der Arbeit mit LINQ beachtet.

Nehmen wir den folgenden Code:

// let's generate some items in a simple list 

var myEnum = new List<string> { "a", "b", "c", "d", "e" };

// now we order the list 

var myOrderedList = from x in myEnum orderby x.CustomOrderBy() select x;

Wir legen also eine Liste an und fragen alle Elemente in einer bestimmten Reihenfolge ab. Relativ simples Code-Konstrukt. Um nun mit den Daten etwas zu machen und über das Hello-World-Szenario hinaus zu gehen, fragen wir unsere sortierte Liste mit einem where-Statement ab und lassen uns das erste Element geben – das Ganze bringen wir noch in einem Loop unter:

// lets enumerate the list items 

foreach (var listItem in myEnum)   

{

  // for each of the elements filter the list for all items 

  // "less" than the current item 

  var myItem = (from x in myOrderedList

                where x.CustomWhere(listItem)

                select x).FirstOrDefault();   

  Console.WriteLine(myItem);

}

Die Methoden CustomWhere und CustomOrderBy sind trivial in Form von Extension Methods implementiert und nur für das Zählen der Aufrufe gedacht:

public static class Extensions

{

    private static int WhereCounter;

    private static int OrderByCounter;

 

    public static string CustomOrderBy(this string value)

    {

        OrderByCounter++;

        Console.WriteLine("CustomOrderBy: {0} Aufrufe", OrderByCounter);

        return value;

    }

 

    public static bool CustomWhere(this string value, string compareto)

    {

        WhereCounter++;

        Console.WriteLine("CustomWhere:   {0} Aufrufe", WhereCounter);

        return value == compareto;

    }

}

Wie im Code zu sehen, habe ich zwei Methoden in der Sortierung und in der Filterung eingesetzt – diese zählen einfach die Aufrufe, so dass wir überprüfen können, wie die Linq-Statements ausgeführt werden:

CustomOrderBy: 25 Aufrufe
CustomWhere:   15 Aufrufe

Whow! 25 Aufrufe für CustomOrderBy – würde Linq mit einem simplen Sortierungs-Algorithmus die das orderby-Statement ausführen, wäre 5 zu erwarten … es gibt ja nur eine Stelle, an der einmalig die Sortierung durchgeführt wird.

Aber: das LINQ-Statement iteriert und sortiert eben NICHT die Liste; es generiert eine Klasse, welche eine sortierte IEnumerable<string> zurück liefert. Also wird die LINQ-Abfrage jedes Mal durchgeführt, wenn sie angesprochen wird – in unserem Fall fünf Mal innerhalb des ForEach-Statements = 25 Ausführungen des darin enthaltenen where-Statements.

Eine simple Lösung des Problems könnte folgendermaßen aussehen:

var myOrderedList = (from x in myEnum orderby x.CustomOrderBy("d") select x).ToArray();

Der Aufruf von .ToArray() wandelt das IEnumerable<string> einmalig (außerhalb des ForEach) in ein echtes Array mit den sortierten Entitäten. Damit muss nicht mehr bei jedem Zugriff die Liste erneut sortiert werden.

Über diese „Feinheiten“ von LINQ sollte man sich im Klaren sein, wenn man LINQ verwendet. Beide Verhaltensweisen (IEnumerable und List) haben Vor- aber auch Nachteile.