CodeGym /Kurse /C# SELF /Optimierung der Arbeit mit großen Dateien

Optimierung der Arbeit mit großen Dateien

C# SELF
Level 41 , Lektion 4
Verfügbar

1. Einführung

Wenn du mit kleinen Dateien arbeitest, merkst du oft nichts: geschrieben, gelesen — und vergessen. Aber sobald die Dateigröße anfängt, zumindest 100–500 MB zu überschreiten, und erst recht bei Gigabytes, treten interessante Effekte auf:

  • Operationen werden langsamer, besonders wenn man "direkt" arbeitet — zum Beispiel fangen File.ReadAllBytes() oder File.WriteAllText() plötzlich an, das ganze Programm zu bremsen.
  • Es kann an Arbeitsspeicher fehlen, es tritt OutOfMemoryException auf.
  • Das System beginnt, swap zu benutzen, was das gesamte System verlangsamen kann.
  • Parallele Operationen können übermäßige Last auf die Festplatte bringen.

In realen Aufgaben trifft man das oft:

  • Server-Logs (Gigabytes am Tag).
  • Verarbeitung großer CSV- oder XML-Dateien beim Export/Import.
  • Arbeit mit Video, Audio, Archiven, binären Dateien.
  • Kopieren großer Builds oder Backup-Erstellung.

2. Optimierungsstrategien bei der Arbeit mit großen Dateien

Bevor du mit Optimieren loslegst — klär zuerst, was genau du beschleunigen oder verbessern willst. Das sind die häufigsten Aufgaben:

  • „Eine Datei so schnell wie möglich lesen/schreiben und das System nicht killen“.
  • „Die Datei stückweise verarbeiten, damit der Speicher nicht voll läuft“.
  • „Keine unnötigen Kopien der Daten im Speicher erzeugen“.
  • „Große Dateien parallel verarbeiten (wenn möglich)“.

Allgemeine Ansätze:

  • Streaming-Lesen und -Schreiben: benutze Streams und Puffer, lese/schreibe in Stücken (FileStream, BufferedStream).
  • Operationen direkt auf der Festplatte — ohne Zwischenkopien im Speicher.
  • Sorgsamer Umgang mit Puffern und Speicher: halte nicht die ganze Datei im RAM.
  • Asynchrone Operationen — wenn der Hauptthread nicht blockiert werden soll (mehr dazu in der nächsten Vorlesung).

3. Streaming-Lesen und -Schreiben: Basis-Pattern

Grundprinzip: Arbeite mit Dateien in Stücken! In modernem C# geht das leicht mit Klassen wie FileStream, BufferedStream und generell über Streams.


// Öffnen des Streams zum Lesen:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;

// Lesen, bis das Ende der Datei erreicht ist
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
    // Hier verarbeitest du die gelesenen Daten!
    // Zum Beispiel berechnen wir die Summe aller Bytes (nur als Übung)
    long sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"Gelesen {bytesRead} Bytes, Summe: {sum}");
}

Tipp: Wähle die Puffergröße (z. B. 64 KB, 128 KB, 1 MB) experimentell. Zu klein — viele Festplattenzugriffe. Zu groß — nicht immer schneller, verbraucht aber mehr Speicher.

Warum so? Klassisches Lesen mit File.ReadAllBytes() oder File.ReadAllText() ohne Aufteilung versucht, den ganzen Inhalt in den Speicher zu laden. Bei riesigen Dateien ist das Ergebnis offensichtlich: OutOfMemoryException.

4. BufferedStream: warum und wann nützlich

Wir sind dieser Klasse schon in einer früheren Vorlesung begegnet, aber kurz zur Erinnerung: BufferedStream ist ein Wrapper um jeden beliebigen Stream, der das Lesen/Schreiben nicht Byte für Byte, sondern blockweise ermöglicht.

Beispiel:


using var fileStream = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
using var bufferedStream = new BufferedStream(fileStream, 1024 * 128);

byte[] buffer = new byte[1024 * 128];
int bytesRead;
while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0)
{
    // Datenverarbeitung
}

In manchen Fällen bringt BufferedStream Beschleunigung, besonders wenn du wenige Bytes auf einmal liest und das Dateisystem blockweise arbeitet.

Interessante Info: In neueren .NET-Versionen hat FileStream bereits eigene Pufferung, daher bringt manuelles Einwickeln in BufferedStream den größten Nutzen bei "rohen" Streams, z. B. bei Netzwerk- oder speziellen Geräte-Streams.

5. Lesen und Schreiben großer Textdateien

Bei binären Dateien ist alles klar: lesen und schreiben in Blöcken. Aber wie mit Textdateien, besonders großen CSV-, Log- oder JSON-Dateien umgehen?

Hier helfen StreamReader zum Lesen und StreamWriter zum Schreiben.

Zeilenweises Lesen:


using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    // Zeile verarbeiten
    if (line.Contains("ERROR"))
        Console.WriteLine("Gefunden ERROR: " + line);
}

Warum das gut ist?

  • Wir halten nicht die ganze Datei im Speicher.
  • Die Pufferung in StreamReader ist bereits gut eingestellt.

Zeilenweises Schreiben:


using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
    writer.WriteLine($"Das ist Zeile Nummer {i}");

Architektur des Streaming-Lesens


   [Datei auf der Festplatte]
          |
     [FileStream]
          |
  [BufferedStream (optional)]
          |
   [StreamReader/StreamWriter (für Text)]
          |
   [Dein Code: Datenverarbeitung]

6. Praktische Anwendung

Wir bauen unsere Anwendung zur Verarbeitung von Logdateien weiter. Angenommen, das Archivieren alter Logs (älter als 7 Tage) soll nicht nur durch Verschieben, sondern durch Komprimieren in ein Archiv geschehen. Bei riesigen Dateien lesen und schreiben wir stückweise, damit der Speicher nicht überläuft.

Zur Vereinfachung verwenden wir standardmäßiges Kopieren mit Streams:


void CopyLargeFile(string sourcePath, string destPath)
{
    using var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
    using var destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
    byte[] buffer = new byte[1024 * 256]; // 256 KB
    int bytesRead;
    while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
    {
        destStream.Write(buffer, 0, bytesRead);
        // Hier kann ein Progressbar eingebaut werden!
    }
}

Wo wird das verwendet?

  • Kopieren von Backups
  • Zusammenführen von Logs nach Tagen oder Monaten
  • Vorverarbeitung von Dateien (z. B. Filtern von Zeilen)

7. Wie die Effektivität des Puffers messen?

Manchmal willst du wissen: „Wie viel schneller ist es geworden?“ Das misst du leicht mit Zeitmessung:


var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"Kopiervorgang Dauer: {watch.Elapsed.TotalSeconds} Sekunden");

Typische Puffergrößen:

  • 4 KB — minimaler Block der Dateisysteme.
  • 64 KB/128 KB — in der Praxis meist gute Werte.
  • 1 MB und größer — nur auf sehr schnellen SSDs und bei großen Dateien sinnvoll.

Probiere verschiedene Puffergrößen selbst aus!

8. Suche und Verarbeitung von Daten in großen Dateien

Was, wenn du nicht nur kopieren, sondern in einer großen Datei nach bestimmten Zeilen, Zahlen oder Wortkombinationen suchen musst? Am effizientesten ist das stückweise.

Beispiel: alle Zeilen mit Fehlern in einem riesigen Log finden


using var reader = new StreamReader("server.log");
using var writer = new StreamWriter("errors.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    if (line.Contains("ERROR"))
        writer.WriteLine(line); // Nur relevante Zeilen ins Ergebnis schreiben
}

Dieser Ansatz erlaubt die Verarbeitung von Logs in Gigabyte-Größe, ohne viel RAM zu verbrauchen.

9. Besonderheiten bei sehr großen Dateien (>2 GB)

.NET und Windows kommen gut mit sehr großen Dateien (sogar mehreren Terabytes) klar, wenn man Streaming verwendet. Aber es gibt Nuancen!

  • 32-bit Anwendungen sind auf 2 GB Adressraum beschränkt — verwende x64!
  • Für große Dateien immer eine 64-bit Plattform nutzen (AnyCPU oder x64).
  • Für Dateien größer als 4 GB ist FAT32 ungeeignet — nutze NTFS/exFAT.

Iterative Verarbeitung großer Dateien


+------------------+
|    Start         |
+------------------+
        |
        v
+------------------------------+
| Stream zum Lesen öffnen      |
+------------------------------+
        |
        v
+------------------------------+
| Solange nicht Dateiende      |
+------------------------------+
        |
        v
+---------------------------+
| Blockdaten lesen          |
+---------------------------+
        |
        v
+---------------------------+
| Block verarbeiten         |
+---------------------------+
        |
        v
+--------------------------+
| Nächster Block           |
+--------------------------+
        |
        v
+--------------------+
| Stream schließen   |
+--------------------+

10. Nützliche Hinweise

Datei als Stream: Schlüsselmethoden von FileStream

Methode Beschreibung
Read
Liests einen Teil der Datei in einen Puffer
Write
Schreibt einen Teil des Puffers in die Datei
Seek
Ermöglicht das Springen zu einer bestimmten Position in der Datei
Length
Dateigröße in Bytes
Position
Aktuelle Position in der Datei

Beispiel für die Verwendung von Seek():


using var stream = new FileStream("bigfile.bin", FileMode.Open);
// Springe 1 GB vorwärts!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// Jetzt lesen wir Daten aus der Mitte der Datei!

Wofür nützlich?

  • Indexierung von Dateien
  • Schneller Zugriff auf benötigte Blöcke (z. B. in großen Datenbanken)

Arbeiten mit Dateien in mehreren Threads (Multithreading)

Wenn die Aufgabe es zulässt, kann man große Dateien parallel verarbeiten: z. B. in Blöcke aufteilen und verschiedene Teile gleichzeitig lesen/schreiben. Wichtig ist zu verstehen, dass HDDs bei zufälligen Zugriffen langsam sind, SSDs deutlich schneller.

Für einfache, private Aufgaben: wenn du unsicher bist, arbeite sequenziell. Parallelität lohnt sich eher beim Verarbeiten mehrerer Dateien gleichzeitig als beim Bearbeiten einer einzelnen Datei (sonst ist der Vorteil fraglich).

11. Typische Fehler bei der Arbeit mit großen Dateien

Fehler Nr. 1: die ganze Datei in den Speicher lesen.
Anfänger verwenden oft File.ReadAllBytes() oder File.ReadAllText() auch für riesige Dateien. Wenn die Datei Gigabytes groß ist, stürzt die Anwendung mit einem Fehler ab — nicht genug Speicher. Verwende Streaming.

Fehler Nr. 2: zu kleinen Puffer verwenden.
Mit einem winzigen Puffer zu lesen ist wie Suppe mit einem Teelöffel zu löffeln. Das Programm verbringt viel Zeit mit Festplattenzugriffen und wartet mehr, als es arbeitet. Wähle eine sinnvolle Puffergröße.

Fehler Nr. 3: Stream nicht schließen.
Wenn du den Stream nach der Arbeit nicht schließt, bleibt der File-Handle belegt. Das stört andere Programme und kann OS-Level-Fehler verursachen. Verwende immer using — so ist es sicherer und sauberer.

Fehler Nr. 4: gleichzeitiger Zugriff auf dieselbe Datei.
Versuche nicht, dieselbe Datei gleichzeitig aus unterschiedlichen Teilen des Programms zu lesen und zu schreiben — das ist ein sicherer Weg zu IOException. Auch wenn es manchmal "funktioniert", erreichst du damit keine Stabilität.

2
Aufgabe
C# SELF, Level 41, Lektion 4
Gesperrt
Kopieren einer großen Datei
Kopieren einer großen Datei
1
Umfrage/Quiz
Prinzip der Datenpufferung, Level 41, Lektion 4
Nicht verfügbar
Prinzip der Datenpufferung
Optimierung von Ein-/Ausgabe
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION