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 |
|---|---|---|---|
|
Vorhanden (4096 Byte) | Ja (über Konstruktor) | Basis-Dateistream |
|
Ja (4096 Byte) | Ja (über Konstruktor) | „Wrapper“ um einen Stream |
|
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:
- Ohne Puffer — byteweise (schlecht, aber illustrativ!)
- Standardpuffer — der normale FileStream mit CopyTo
- 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 4–8 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.
GO TO FULL VERSION