Laden...

Enumerable.Empty<T> oder Array.Empty<T>

Erstellt von Abt vor einem Jahr Letzter Beitrag vor einem Jahr 499 Views
Hinweis von gfoidl vor einem Jahr

Abgeteilt von Entsteht ein Memory Leak beim zuweisen einer Auflistung

Abt Themenstarter:in
16.842 Beiträge seit 2008
vor einem Jahr

Hier noch ein Benchmark dazu:


BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1706 (21H2)
.NET SDK=6.0.300
  [Host]     : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT


| Method |      Mean |     Error |    StdDev |  Gen 0 | Allocated |
|------- |----------:|----------:|----------:|-------:|----------:|
|   List | 14.574 ns | 0.2194 ns | 0.2052 ns | 0.0043 |      72 B |
|  Array | 10.163 ns | 0.1851 ns | 0.1732 ns | 0.0014 |      24 B |
|  Empty |  4.739 ns | 0.0956 ns | 0.0894 ns |      - |         - |

Code: SustainableCode/csharp/create-empty-collection at main · BenjaminAbt/SustainableCode

2.080 Beiträge seit 2012
vor einem Jahr

Ich nutze immer: Array.Empty<string>()
Dabei wird nur einmal ein leeres Array erzeugt, danach wird es statisch vorgehalten
Müsste ja also ungefähr das gleiche sein, wie Enumerable.Empty<string>(), da wird ja auch ein leeres Objekt erzeugt und dann statisch vorgehalten.
Ein Benchmark habe ich aber noch nicht gemacht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Abt Themenstarter:in
16.842 Beiträge seit 2008
vor einem Jahr

Korrekt, Enumerable.Empty<string>() verweist quasi auf einen Singleton.
Das heisst, dass prinzipiell schon ein Objekt unter der Haube existiert, aber eben keines erzeugt wird.
https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Core/System/Linq/Enumerable.cs#L2147

Das gleiche passiert bei Array.Empty
https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/mscorlib/system/array.cs#L3080

Edit, hab mal Array.Empty als Vergleich dazu gepackt.
Der Time-Punch beim Array kommt (wahrscheinlich) durch das Cast von Array auf IEnumerable zustande.


BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1706 (21H2)
.NET SDK=6.0.300
  [Host]     : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT

|          Method |      Mean |     Error |    StdDev |  Gen 0 | Allocated |
|---------------- |----------:|----------:|----------:|-------:|----------:|
|            List | 14.112 ns | 0.2331 ns | 0.2775 ns | 0.0043 |      72 B |
|           Array |  9.364 ns | 0.1745 ns | 0.1632 ns | 0.0014 |      24 B |
|      ArrayEmpty |  7.712 ns | 0.0427 ns | 0.0378 ns |      - |         - |
| EnumerableEmpty |  4.690 ns | 0.0252 ns | 0.0223 ns |      - |         - |

SustainableCode/csharp/create-empty-collection at main · BenjaminAbt/SustainableCode

2.080 Beiträge seit 2012
vor einem Jahr

Der Time-Punch beim Array kommt (wahrscheinlich) durch das Cast von Array auf IEnumerable zustande.

Immer wieder erstaunlich, wo sich Details verstecken können, das hätte ich nicht erwartet 😁
Also in Zukunft lieber Enumerable.Empty nutzen - tut nicht weh und ist schneller.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

6.911 Beiträge seit 2009
vor einem Jahr

Hallo,

Also in Zukunft lieber Enumerable.Empty nutzen - tut nicht weh und ist schneller.

"Initialisierung" mit null ist am schnellsten und effizientesten.

Je nach folgender Verwendung kann die Initialisierung per Sentinel wie Array.Empty<T> od. Enumerable.Empty<T> sinnvoll sein, da so der null-Check entfällt.
Allerdings kann auch für eine leere Menge (wie sie Array.Empty und Enumerable.Emtpy bereit stellen) bei Verwendung z.B. in foreach od. anderen Linq-Methoden teil beträchtlicher Overhead ausgeführt werden nur um letztlich festzustellen, dass es eine leere Menge ist. Da kann ein null-Check effizienter sein.

Es kommt jedoch sehr darauf an was man will (und wie die Benchmarks dafür gestaltet werden).

Bei


List<int> list = ?
int sum = Sum(list);

static int Sum(List<int>? list);

hängst es von der Implementierung von Sum ab, wie list initialisiert wird.

Am effizientesten hier ist wohl:


List<int>? list = null;

static int? Sum(List<int>? list)
{
    if (list is null) return null;

    // Summe bilden
}

, da der null-Check sehr effizient ist (im Maschinencode nur ein test rdx, rdx) und entweder keine Schleife für die Summe od. kein Iterator für die Summe erstellt / ausgeführt werden muss.
Dies geht allerdings nur, wenn wie hier der Rückgabewert geändert werden kann bzw. die "Sentinel-Prüfung" verschoben wird.
Hier konkret wird int? zurückgegeben, d.h. es müsste an anderer Stelle geprüft werden ob HasValue true ist.

Was ich damit v.a. ausdrücken will: die zitierte Aussage ist pauschal falsch -- es kommt vielmehr auf den konkreten Fall darauf an.

Zurück zum ganz ursprünglichen Problem vom OT:

  
public class TestClass  
{  
  public string Name { get; set; }  
  
  public IEnumerable<string> Lines { get; set; } = new List<string>();  
}  
  

hier würde ich einfach


public class TestClass
{
	public string Name { get; set; }

	public IEnumerable<string> Lines { get; set; }
}

verwenden.
Es schreibt ja auch niemand


public class TestClass
{
	public string Name { get; set; } = "Dummy Name der dann eh ersetzt wird, aber nur damit es halt nicht null ist --> Sentinel";

	public IEnumerable<string> Lines { get; set; } = Array.Empty<string>();
}

(gut vllt. etwas realer mit string.Empty).

Der Time-Punch beim Array kommt (wahrscheinlich) durch das Cast von Array auf IEnumerable zustande.

Im Benchmark-Code finde ich Consumer und Consume nicht, daher keine Ahnung warum.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

2.080 Beiträge seit 2012
vor einem Jahr

Was ich damit v.a. ausdrücken will: die zitierte Aussage ist pauschal falsch -- es kommt vielmehr auf den konkreten Fall darauf an.

Mir ging es mit der Aussage mehr um eine allgemeine Faustregel, die man sich für alle nicht kritischen Fälle angewöhnen kann.
Vielleicht ist Enumerable.Empty<T> nicht in jeder Situation das performanteste, aber wenn man IEnumerable<T> hat, ist es performanter, als Array.Empty<T>.

Einfach pauschal null nehmen, würde ich aber nicht, da der Kontext ggf. gar kein null erlaubt und man somit den Nutzer des Codes dazu zwingen würde, immer auf null zu prüfen.
Da kann ein Enumerable.Empty<T> zwar langsamer sein, aber man muss die null-Checks nicht schreiben und nicht überall ein Verhalten im null-Fall definieren.
Wenn nun der Code, der ein IEnumerable<T> zurück liefert, nie null zurück liefert, ist das Verhalten im Fall, dass es keine Ergebnismenge gibt, automatisch klar definiert.
Aber das bezieht sich natürlich nicht auf die Fälle, in denen man auf diesem Level optimieren muss 😉

Ist aber dennoch gut zu wissen - besonders das Detail, wie performant der null-Check ist

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

6.911 Beiträge seit 2009
vor einem Jahr

Hallo,

es gibt noch einen Punkt der bei Enumerable.Empty<T> beachtet werden soll bzw. in Zukunft noch wichtiger erscheinen wird.

Nämlich dass es sich dabei um eine Linq-Methode handelt, d.h. es wird Linq ins Projekt hineingeholt und das erzeugt einen größeren "Fussabdruck" der Anwendung in Hinsicht auf Ressourcen-Verbrauch.

In Zukunft wird v.a. das Thema IL-Trimming immer bedeutender, daher ist Linq hier eher ein Antagonist und wird / sollte womöglich vermieden werden, falls statischer Fussabdruck der App ein Kriterium ist.
Dies geht i.d.R. auch einher mit Startzeit der App (wichtig v.a. bei serverless Umgebungen, etc.).

Array.Empty<T> ist direkt in System.Private.CoreLib, daher ist es nicht so gewichtig wie Linq, hat aber auch seinen Fussabdruck, da es generisch ist und kann so u.U. ein paar Instanziierungen haben (1 für alle Referenztypen und eine je Werttyp).

Das soll jetzt nicht heißen, dass Linq per se vermieden werden soll, denn es is ja durchwegs sehr praktisch um kurzen leserlichen Code zu schreiben, aber es sollte auch nicht außer Acht gelassen werden, was es bedeutet solche Typen und Konstrukte zu verwenden.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Abt Themenstarter:in
16.842 Beiträge seit 2008
vor einem Jahr

einen größeren "Fussabdruck" der Anwendung in Hinsicht auf Ressourcen-Verbrauch.

Was ist die Definition von Ressourcen in diesem Fall? Der CPU Zyklus beim Start? Der RAM-Eintrag für das Singleton?

Ich mein, .NET geht Dev-freundlich den Weg von Globalen Usings; Linq ist Teil davon.
Trimming hat sicherlich einen messbaren Effekt in gewissen Szenarien - aber diese sind dann in der .NET Welt prozentual super selten (leider).
In der Masse macht das alles sicherlich einen Unterschied; eine Referenz, die ohnehin als Basis gilt - eher nicht.

Und für den 0815 Fall, der vermutlich 99,99% der Use Cases abdeckt, geh ich dann doch lieber den Weg über Enumerable.Empty<T> statt überall mit Nullable zu arbeiten, den es dann nur wegen des künstlichen Falls gibt.
Letzteres ist dann - wenn es sich nur auf diesen Fall bezieht - schon Over Engineering.

6.911 Beiträge seit 2009
vor einem Jahr

Hallo Abt,

Code sollte v.a. les- und wartbar (sowie testbar) geschrieben werden. Dabei sollen und dürfen natürlich Konstrukte verwendet werden, welche dieses Ziel unterstützen.
Da sind wir uns sicher einig.

Nachhaltiger Code, im Sinne von Vermeidung unnötiger Ressourcen, wird (glücklicherweise) immer bedeutender*. Daher wollen wir auch ein wenig über den Standardhorizont hinausblicken und ein wenig Bewusstsein dafür schärfen. Ich nehme an daher gibt es auch https://github.com/BenjaminAbt/SustainableCode.

Trimming ist momentan eine Nische. Mit ein Grund dafür ist wohl auch, dass viele Teile vom .NET Framework noch nicht ganz trimm-freundlich sind** -- das wird sich mit .NET 7 und späteren Versionen bessern. Weiters dass wohl auch etliche Anwendungen framework-dependent laufen, daher Trimming untergeordnet ist.

Genauso ist (native) AOT (ahead of time compilation -- im Gegensatz zu JIT (just in time compilation)) noch eher eine Nische und in den Startlöchern.
Bisher war die Tool-Unterstützung dafür auch eher mangelhaft, aber das bessert sich gerade.

AOT und Trimming gehen Hand-in-Hand und sollten im Idealfall nativen Code für die gewählte Plattform produzieren, so dass* keine Assemblies in IL-Code -- od. wie bei R2R (ready to run) mit IL-Code und Maschinencode -- vorliegen, sondern eben direkt ausführbarer Maschinencode

  • kein JIT nötig ist, da der Code schon zur Build-Zeit komplett übersetzt wurde
  • in den "binaries" nur jene Code-Teile vorhanden sind, die tatsächlich benötigt werden
  • Code kann besser optimiert werden (eher für zukünftige .NET Versionen geplant)

Nachteilig bei AOT ist, dass der alte .NET-Grundsatz (der eigentlich erst durch .NET Core galt) "compile once, run everywhere" nicht mehr gilt, da bei der Kompilierung die Zielplattform bekannt sein muss.

Aber dadurch ergeben sich ein paar Vorteile bzgl. Ressourcen, nämlich* wesentlich kleiner Binaries

  • benögiten weniger Speicherplatz -- ist für viele kein Kriterium, aber in Hinsicht Blazor / Webassembly nicht unkritisch

  • das OS muss weniger Daten in den RAM laden --> weniger RAM-Belastung und auch schneller

  • kein JIT nötig --> zusammen mit den weniger Daten die zu laden sind, (viel) schnellerer Start der Anwendung -- bei lang laufenden Diensten nicht so wichtig, dafür aber bei Apps die oft gestartet werden (serverless, Container-Lösungen, ...)

Je nach Ziel-Plattform (klassischer Server, Cloud / Serverless, Mobil, etc.) sind die Einsparungen mehr od. weniger eklatant und relevant.
SustainableCode (Link siehe oben) beschreibt hier ein wenig mehr warum (für alle die das noch nciht gesehen haben).

In .NET beginnt die Reise mit AOT erst. Daher sollten wir auch jetzt schon einen Blick darauf werden und frühzeitig bestimmte Dinge berücksichtigen, dann ist es später einfacher.
Das soll jetzt aber keinesfalls eine Aufforderung sein, Linq durch manuell geschriebene Schleifen zu ersetzen. Es soll Linq dort verwendet machen, wo es sinnvoll ist. Jetzt und in Zukunft.

Aber es sollte nicht die Abhängigkeit zu Linq ins Projekt gebracht werden, nur um Enumerable.Empty<T> verwendet zu können.
Falls Linq ohnehin verwendet wird, so kein Problem. Anderfalls ist das in Hinsicht AOT hinderlich.
Da wäre ev. Array.Empty<T> passender, da diese im ausgeführten .NET Code-Pfad mit hoher Wahrscheinlichkeit sowieso dabei ist. (Klar, geht es dabei auch wieder darum, wie es verwendet wird (siehe Benchmarks oben, sofern korrekt) um nicht durch die Verwendung in der App mehr CPU-Ressourcen zu benötigen als vorher eingespart wurden.

.NET geht Dev-freundlich den Weg von Globalen Usings; Linq ist Teil davon.

Solange Linq nicht tatsächlich verwendet wird, so erstellt der Kompiler auch keine Referenz zu Linq in der Assembly. Das zählt.
Das gilt für alle Paketreferenzen allgemein: falls kein Typ einer Referenz verwendet wird, so ist in der Assembly diese Referenz nicht vorhanden.
Somit haben global Usings keinen negativen Effekt diesebezüglich.

den es dann nur wegen des künstlichen Falls gibt.

Beim "künstlichen Fall" kann ich jetzt nicht folgen.
Dass mit Linq und v.a. durch dessen generische Art, jede Menge Instanzen erstellt werden ist real und nicht künstlich 😉 (falls das gemeint sei)

* auch wenn sich wesentlich mehr Ressourcen-Einsparungspotential durch Vermeidung von Video-Streaming, Gaming, Social-Media, etc. ergibt, zählt doch jeder kleine Beitrag ein bischen...

** Z.B. ist Reflektion und Code-Erzeugung zur Laufzeit trimmer-unfreundlich, jedoch basieren etliche Konzepte in .NET darauf, z.B. ASP.NET Core Dependency Injection.
Durch die Einführung der Roslyn Sourcegenerators, konnte schon viel reflektion-basiertes umgeschrieben werden. Aber bei weitem noch nicht alles, das dauert halt.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

6.911 Beiträge seit 2009
vor einem Jahr

Hallo,

persönlicher Nachtrag:
Ich hab keine primäre Motivation Code zu schreiben, der extra Ressourcen-schonend ist und so z.B. auch für weniger CO2-Emissionen sorgt.
Jedoch finde ich es gut, wenn im Hinterkopf / Unterbewusstsein ein paar Gedanken in dieser Richtung vorhanden sind. Dadurch wird dieses Ziel irgendwie auch unbewusst verfolgt.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Abt Themenstarter:in
16.842 Beiträge seit 2008
vor einem Jahr

Solange Linq nicht tatsächlich verwendet wird, so erstellt der Kompiler auch keine Referenz zu Linq in der Assembly.

Das ist korrekt. Aber wie sieht die Realität aus?
De facto (>95%?) der Anwendungen nutzt implizit oder explizit gewisse Namespaces - und diese sind in den Default der Global Usings gelandet.
Das wurde ja evaluiert.
Das sieht bei Bibliotheken selbstverständlich völlig anders aus.

Aber es sollte nicht die Abhängigkeit zu Linq ins Projekt gebracht werden, nur um Enumerable.Empty<T> verwendet zu können.

.. ist daher inhaltlich korrekt. Aber aus Realitätssicht ist es einfach <quasi> immer schon da.

Ich würde mich - Dich sowieso bzw. 10 mal eher - als einen Entwickler sehen, der durchaus in Nischen-Umgebungen unterwegs ist, wo das wirklich eine Rolle spielt.
Für 99,99% der Entwickler:innen, wenn nicht gar höher, ist es aber schon ein immenser Fortschritt Enumerable.Empty zu verwenden, statt dauernd Listen zu allokieren.
Eine null-Optimization ist dagegen Raketentechnik. AOT wird da die nächsten 15 Jahre nicht aus eigenem Antrieb ankommen.

Obwohl ich selbst schon durchaus auch darauf achte, wie Code geschrieben ist, und was darunter passiert, muss ich Kompromisse machen, weil es sonst zB. Kolleg:innen oder Kund:innen trotz 20 Jahre .NET Erfahrung nicht verstehen. Es spielt in vielen Welten einfach keine Rolle. Da wird mit Allokations, TCP-Verbindungen, Instanzen und Co einfach um sich geworfen - und es funktioniert trotzdem.

Ich selbst optimier Anwendungen auch nur dann, wenn es sinn macht - und es die Kolleg:innen noch nachvollziehen können.
Und wenn ich an einem Level ankommen bin, wo eine Optimierung keine Auswirkung mehr hat auf die Ressourcen (zB. weil ich in Azure pro Instanz nicht kleiner gehen kann) - ist jede weitere Optimierung inhaltlich unsinnig, leider.

Ich hab keine primäre Motivation Code zu schreiben, der extra Ressourcen-schonend ist und so z.B. auch für weniger CO2-Emissionen sorgt.
Jedoch finde ich es gut, wenn im Hinterkopf / Unterbewusstsein ein paar Gedanken in dieser Richtung vorhanden sind. Dadurch wird dieses Ziel irgendwie auch unbewusst verfolgt.

Das ist der Grund, wieso es GitHub - BenjaminAbt/SustainableCode: Sustainable Code Samples and Patterns by BEN ABT gibt. Für das Bewusstsein.