1. Einführung
Bevor man eine neue Klasse verwendet, sollte man verstehen, wozu sie überhaupt da ist. Schauen wir uns an, was passiert, wenn wir direkt mit einer Datei über FileStream arbeiten.
Wenn du Read oder Write auf einem Stream aufrufst, der mit FileStream erstellt wurde, gibt es tatsächlich einen Zugriff auf die Festplatten- bzw. Speichersubsysteme des Computers. Dieser Vorgang ist an sich (besonders auf alten HDDs, aber auch auf modernen SSDs) viel langsamer als Arbeit mit RAM. Stell dir vor, wenn du bei McDonalds Pommes bestellst, rennt der Kassierer jedes Mal in den Lagerraum, um eine neue Tüte zu holen. Stell dir vor, wie lang die Schlange wird!
Wenn man mit kleinen Datenstücken arbeitet, führen häufige Zugriffe auf Festplatte oder Netzwerk zu Leistungsverlust. Je größer das Datenvolumen, desto deutlicher der Effekt.
Kurze Analogie
Streams ohne Buffer sind ungefähr so, als würdest du zehnmal zum Laden fahren, um jeweils einen Joghurt zu kaufen. Ein gepufferter Stream ist, wenn du gleich einen ganzen Korb Joghurt nimmst und die Fahrten auf ein Minimum reduzierst.
2. Klasse BufferedStream: erster Blick
Wozu sie dient
BufferedStream ist eine Hülle um jeden Stream (Stream), die einen Zwischenspeicher im Arbeitsspeicher hält. Wenn du Daten schreibst, landen sie zuerst im Puffer und werden erst dann auf die Festplatte in einer großen Operation geschrieben, wenn der Puffer voll ist. Ähnlich beim Lesen: beim ersten Lesen lädt er einen größeren Datenblock in den Speicher und liefert dann Stück für Stück aus dem Speicher, bis der Puffer leer ist.
Codebeispiel: Erstellen eines BufferedStream
Lass uns ein einfaches Beispiel bauen. Angenommen, wir müssen 100.000 Zeilen in eine Datei schreiben:
string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);
for (int i = 0; i < 100_000; i++)
{
writer.WriteLine($"Stroka nomer {i}");
}
Console.WriteLine("Schreiben abgeschlossen!");
Kommentar:
- Wir öffnen die Datei zum Schreiben über FileStream.
- Dann wickeln wir ihn in einen BufferedStream und danach in einen StreamWriter (er schreibt Textzeilen in einen Stream).
- Sobald der Puffer voll ist, werden die Daten in einem Rutsch auf die Festplatte geschrieben.
3. Wie Pufferung „unter der Haube“ funktioniert
Schauen wir uns das in einer einfachen Darstellung an:
[Dein Code] → [StreamWriter] → [BufferedStream] → [FileStream] → [Datei auf der Festplatte]
Wenn du WriteLine() beim StreamWriter aufrufst, wird der Text zuerst in dessen internen Puffer geschrieben, dann durch den BufferedStream in einen weiteren Puffer gelegt und erst wenn der Puffer voll ist oder der Stream geschlossen wird, landen die Daten auf der Festplatte.
Wie viele Bytes passen in einen Eimer?
Die Standard-Puffergröße ist 4096 Bytes (4 KB), aber man kann sie explizit angeben:
int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...
Praktischer Tipp: Auf modernen Systemen sind Puffer im Bereich 8–64 KB oft sinnvoll. Bei sehr großen Dateioperationen kann man noch größer gehen. Aber übertreibe es nicht: wenn du auf einem Mikrocontroller mit 128 KB RAM arbeitest, ist ein 64 KB Puffer keine gute Idee :)
4. Experiment: Geschwindigkeit mit und ohne Puffer vergleichen
Um zu verstehen, wie wichtig das ist, schreiben wir einen Test, der Schreibvorgänge mit FileStream mit und ohne Puffer vergleicht:
using System.Diagnostics;
using System.Text;
string data = new string('X', 1000); // 1 000 Zeichen
void WriteWithoutBuffer()
{
using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
fs.Write(bytes, 0, bytes.Length); // Direkt in die Datei – jedes Mal Zugriff auf die Festplatte
}
}
void WriteWithBuffer()
{
using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
using var bs = new BufferedStream(fs, 16 * 1024);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
bs.Write(bytes, 0, bytes.Length);
}
}
// Zeit messen
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Ohne Puffer: " + sw.ElapsedMilliseconds + " ms");
sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("Mit Puffer: " + sw.ElapsedMilliseconds + " ms");
Erwartete Ausgabe:
In den meisten Fällen wirst du mit Puffer einen deutlichen Geschwindigkeitsgewinn sehen! Besonders bei HDDs. Bei SSDs ist der Effekt immer noch vorhanden, aber weniger dramatisch.
5. Welchen Puffer wählen? Vergleich und Praxis
In .NET gibt es viele Klassen zur Pufferung. Lass uns Klarheit schaffen:
| Klasse | Wofür | Puffer eingebaut? | Sollte man BufferedStream verwenden? |
|---|---|---|---|
|
Arbeit mit Dateien | Ja (4 KB) | Meist nicht nötig (aber möglich) |
|
Arbeit mit Netzwerk | Nein | Sehr empfehlenswert |
|
Lesen/Schreiben von Text | Ja (ab ~1 KB) | Normalerweise nicht nötig |
|
Kompression/Dekompression | Nein | Kann sinnvoll sein/ist oft nötig zur Beschleunigung |
Wichtig:
Ein FileStream mit dem Konstruktorparameter bufferSize ist im Grunde bereits gepuffert. Wenn du selbst einen ausreichend großen Puffer angegeben hast, bringt ein zusätzlicher BufferedStream meist keinen großen Vorteil. Wenn du allerdings mit einem anderen Stream (z. B. Netzwerkstream) arbeitest, ist BufferedStream sehr hilfreich.
6. Beispiel: Datei kopieren mit BufferedStream
string source = "big_input.dat";
string dest = "big_output.dat";
int bufferSize = 64 * 1024; // 64 KB
using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
bufferedOutput.Write(buffer, 0, bytesRead);
}
// Nicht vergessen zu flushen – sonst kommen die letzten Bytes nicht auf die Festplatte!
bufferedOutput.Flush();
Console.WriteLine("Kopieren abgeschlossen!");
Kommentar:
- Wir lesen große Blöcke (64 KB) aus einer Datei über BufferedStream.
- Schreiben sie in eine andere Datei, ebenfalls gepuffert.
- Am Ende des Loops unbedingt Flush() aufrufen, damit die letzten Daten geschrieben werden.
7. Nützliche Feinheiten
Tipp: Wann BufferedStream wirklich nützlich ist
- Wenn du mit Streams arbeitest, die keine Pufferung haben (z. B. NetworkStream oder eigene Stream-Implementierungen);
- Wenn du mit großen Mengen binärer Daten arbeitest (z. B. Datei-Kopien, Formatkonvertierungen, Backups);
- Wenn du bestehenden Code optimierst und siehst, dass viele kleine Write/Read-Operationen der Flaschenhals sind.
Ein bisschen zu Asynchronität und Pufferung
Mit dem Aufkommen von asynchronen Operationen (ReadAsync/WriteAsync) bleibt Pufferung nützlich, aber denk dran: wenn du asynchrone Methoden über einem Puffer verwendest, läuft die Verarbeitung zunächst im Speicher, und die physikalischen Zugriffe auf die Festplatte werden weiter minimiert.
In .NET 8+ und .NET 9 wird Pufferung immer tiefer integriert, und die meisten Klassen haben standardmäßig Puffer. Für Kompatibilität mit Netzwerkstreams oder eigenen Implementierungen ist es aber weiterhin sinnvoll, BufferedStream bei Bedarf manuell zu verwenden.
Mehr zu Asynchronität lernst du in Level 58 :P
Visuelle Darstellung der gepufferten Streams
flowchart LR
A[Dein Code] --> B[StreamReader/Writer]
B --> C[BufferedStream]
C --> D[FileStream]
D --> E[Datei/Gerät]
- A — Dein Code, der Write/Read aufruft.
- B — High-Level-Stream (arbeitet mit Text oder Daten).
- C — Pufferung (gruppiert Daten zur Performance-Steigerung).
- D — Konkrete Stream-Implementierung (Datei, Netzwerk).
- E — Physisches Gerät (HDD, SSD, Netzwerk etc.).
Tipps und Tricks aus der Praxis
- Wenn du Zeile für Zeile in eine Datei schreibst (z. B. Logging), gib besser eine Puffergröße an, die größer ist als eine einzelne Zeile. So werden größere Datenpakete schneller ausgeliefert.
- Wenn jede Aktion sofort auf die Festplatte geschrieben werden muss (z. B. kritische Logs), rufe nach jedem Schreibvorgang Flush() auf. Das verringert aber den Vorteil der Pufferung!
- Bei temporären Dateien, die sofort gelöscht werden, ist es manchmal egal, ob etwas im Puffer bleibt — sei aber vorsichtig, wenn es wichtig ist, dass die Datei definitiv geschrieben wurde.
- Bei sehr großen Dateien (z. B. mehrere Dutzend GB) kannst du die Puffergröße ruhig auf 1_048_576 Bytes (1 MB) oder mehr erhöhen — wichtig ist nur, dass genug RAM zur Verfügung steht.
8. Typische Fehler und Fallstricke
Wenn du jetzt Feuer und Flamme bist und überall Puffer reinpflanzen willst — sei geduldig. Alles in Maßen!
Ein häufiger Fehler ist, Flush() oder das Schließen des Streams zu vergessen. Wenn der Stream noch offen ist und das Programm abstürzt, können letzte Bytes im RAM-Puffer verloren gehen und nicht auf die Festplatte geschrieben werden. Zum Beispiel beim Logging kann der letzte Eintrag verschwinden, wenn das Programm abstürzt.
BufferedStream „weiß“ nicht von der Grenze deiner logischen Nachrichten — es wartet einfach, bis genug Daten angesammelt sind. Deshalb ist es für kritische Dinge (Logging, Backups usw.) besser, regelmäßig gezwungen Flush() aufzurufen:
bufferedStream.Flush(); // Zwingt den Puffer, Daten auf die Festplatte zu schreiben
Wenn du StreamWriter verwendest, hat dieser seinen eigenen Puffer! Das bedeutet, dass bei geschachtelter Verwendung die Pufferung doppelt stattfindet (was nicht immer ideal ist). Oft reicht ein einziger Pufferlevel; wenn du StreamWriter nutzt, ist ein zusätzlicher BufferedStream häufig überflüssig.
GO TO FULL VERSION