CodeGym /Kurse /C# SELF /Performance und manuelle Pufferverwaltung

Performance und manuelle Pufferverwaltung

C# SELF
Level 41 , Lektion 3
Verfügbar

1. Einführung

Warum Pufferung wichtig ist und Vergleich der Performance

Wir haben bereits verstanden, dass Pufferung eine Strategie ist, mit der man Daten in größere „Pakete“ bündelt, sodass man nicht hunderttausende Male zur Platte greifen muss, sondern deutlich seltener und dafür gleich große Datenmengen überträgt. Das liefert einen beachtlichen Performancegewinn, besonders bei großen Datenmengen.

Wann man über I/O-Performance nachdenken sollte

  • Wenn ihr mit großen Dateien arbeitet (Gigabytes, Terabytes — z. B. eure Lieblings-Log-Sammlung aus allen Jahren der Firmenarbeit).
  • Wenn minimale Reaktionszeit gefordert ist (z. B. Echtzeit-Logverarbeitung).
  • Wenn die Anzahl der Dateioperationen riesig ist (z. B. Massenumbenennung/-kopie eines Fotoarchivs).
  • Zur Demonstration im Interview, dass ihr moderne Optimierungsansätze versteht (und gern Geschwindigkeit messt, statt einfach „irgendwie zum Laufen zu bringen“).

In .NET sind die meisten File-Streams standardmäßig bereits gepuffert, aber manchmal braucht man eine feine Einstellung oder eine spezielle Strategie.

Die Hauptakteure — welche Puffer es gibt

Klasse Standardpuffer Kann die Größe geändert werden Anwendbarkeit
FileStream
Vorhanden (4096 Byte) Ja (über Konstruktor) Basis-Dateistream
BufferedStream
Ja (4096 Byte) Ja (über Konstruktor) „Wrapper“ um einen Stream
StreamReader/Writer
Ja (1024/1024 Byte) Ja (Konstruktor) Arbeit mit Text
  • BufferedStream kann einen anderen Stream „einpacken“, um die Performance zu verbessern (z. B. wenn der Basestream schlecht gepuffert ist oder ein größerer Puffer gebraucht wird).
  • Die Puffergröße ist ein Kompromiss zwischen Geschwindigkeit und RAM-Verbrauch.

2. Beispiele:

Lasst uns drei Ansätze zum Kopieren einer großen Datei vergleichen:

  1. Ohne Puffer — byteweise (schlecht, aber illustrativ!)
  2. Standardpuffer — der normale FileStream mit CopyTo
  3. Manuelle Pufferverwaltung — wir übergeben den Puffer selbst und optimieren die Größe

Für die Experimente erstellen wir ein kleines Dienstprogramm zum Kopieren von Dateien, das wir in unsere Anwendung einbauen. Die Datei nennen wir BigFile.bin.

class FileCopyBenchmarks
{
    // Kopieren byteweise (Anti-Beispiel — so sollte man es nicht tun!)
    public static void CopyOneByte(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        int b;
        while ((b = input.ReadByte()) != -1)
        {
            output.WriteByte((byte)b);
        }
    }

    // Kopieren mit dem Standardpuffer von FileStream
    public static void CopyWithDefaultBuffer(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        input.CopyTo(output); // Verwendet internen Puffer (meist 81920 Byte)
    }

    // Kopieren mit manueller Pufferkontrolle
    public static void CopyWithCustomBuffer(string source, string dest, int bufferSize = 1024 * 1024)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
        }
    }
}

Welche Funktion ist schneller? Messen wir die Laufzeit.

Wie man Performance korrekt misst

In .NET verwendet man am einfachsten Stopwatch:

static void Measure(Action action, string description)
{
    var sw = Stopwatch.StartNew();
    action();
    sw.Stop();
    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} ms");
}

Jetzt kopieren wir dieselbe Datei auf drei Arten:

string source = "BigFile.bin";
string dest1 = "copy1.bin";
string dest2 = "copy2.bin";
string dest3 = "copy3.bin";

// Erstelle vorher die Datei BigFile.bin (z. B. 100–500 MB) oder nutze eine andere große Datei.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (byteweise)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (standard)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 MB)");

Gefährliche Stellen und Fallstricke

  • Wenn man viele Durchläufe hintereinander macht, kann der OS-Cache die Platte „vorwärmen“ und spätere Messungen werden schneller — für realistische Bewertung besser das Programm neu starten und Cache leeren.
  • Bei kleinen Dateien (10–20 KB) merkt man die Vorteile der Pufferung kaum — je größer die Datei, desto deutlicher der Unterschied.
  • Ein zu großer Puffer (z. B. 100 MB) kann den RAM-Verbrauch stark erhöhen und das restliche System beeinträchtigen.

Visualisierung der Ergebnisse: Tabelle

Methode Zeit (ms) bei 500 MB Datei
Byteweise 100 000+
Standard FileStream / CopyTo 1 000 — 5 000
Manueller Puffer 1 MB 700 — 1 200

Zahlen sind beispielhaft, der Trend bleibt: je größer der Puffer, desto weniger Zugriffe auf die Platte und höhere Geschwindigkeit.

3. Anatomie der manuellen Pufferverwaltung

Warum möchte man manchmal trotzdem die Puffergröße einstellen? Einfacher Vergleich: Du ziehst in eine neue Wohnung um. Du kannst Sachen löffelweise tragen oder gleich einen großen Karton nehmen. Aber auch der Karton hat seine Grenze, sonst kannst du ihn nicht heben!

Wie Lesen mit manuellem Puffer funktioniert

// Beispiel für manuelle Puffergrößenverwaltung
int bufferSize = 1024 * 1024; // 1 MB
byte[] buffer = new byte[bufferSize];
int read;
while ((read = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
    outputStream.Write(buffer, 0, read);
}
  • Die Methode Read versucht, den ganzen Puffer zu füllen, kann aber weniger zurückgeben, wenn die Datei endet.
  • Die Puffergröße wählt man oft zwischen 32 KB und 48 MB — größer bringt meist kaum noch Gewinn.
  • Denkt an den RAM, vor allem wenn viele solcher Operationen oder Threads gleichzeitig laufen.

Experimentieren mit der Puffergröße

Ändert die Puffergröße in unserem Beispiel (32 KB, 128 KB, 1 MB, 4 MB) und schaut, wo die Performance ihren Peak hat. Häufig ist die „goldene Mitte“ bei rund 1 MB.

Szenarien, in denen manueller Puffer besser ist

  • Wenn ihr den RAM-Verbrauch kontrollieren müsst (z. B. auf schwachen Servern).
  • Wenn der Stream nicht automatisch gepuffert wird (NetworkStream, eigene Streams).
  • Bei vielen parallelen Operationen — jedem Thread kann man einen eigenen optimalen Puffer zuweisen.
  • Wenn ihr maximale Beschleunigung für sehr große Dateien wollt (z. B. beim Konvertieren großer CSV-Dateien).

4. Best Practices und typische Fehler

Kann man den Puffer einfach extrem groß machen? Klar, „RAM ist ja reichlich“. Kann man, aber wozu? Zu große Puffer können sogar die Performance verschlechtern: Speicher bleibt ungenutzt blockiert, und das System kann wegen Cache-Problemen ins Stocken geraten.

Manuelle Pufferung beschleunigt nicht alles. Bei kleinen Dateien oder schon optimierten Streams (z. B. FileStream mit großem internem Puffer) ist der Gewinn minimal und der Code wird komplexer.

Typische Falle: vergessen, den Stream zu schließen oder Exceptions nicht zu behandeln — die Datei bleibt gesperrt. Nutzt using und Fehlerbehandlung (try-catch) beim Arbeiten mit Dateien.

2
Aufgabe
C# SELF, Level 41, Lektion 3
Gesperrt
Datei kopieren mit manuellem Puffer
Datei kopieren mit manuellem Puffer
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION