Thomas Kramer

IT-COW | All posts tagged 'NET'

.NET: Vergleich von vier Varianten zum Datei-Laden

By Administrator at Dezember 11, 2011 23:27
Filed Under: .net-Framework, C#, Programmierung allgemein

Ergänzend zu meinen vorherigen Beitrag zum Datei-Lesen in Java habe ich nun auch das Äquivalent für das .NET-Framework programmiert.

 

Wenn man das einmal mit Java vergleicht sieht man dass das .NET-Framework insgesamt bedachter mit dem RAM-Verbrauch umgeht - vor allem ist der benötigte relative Speicher-Mehrverbrauch beim StringBuilder von .NET deutlich geringer wenn wegen zu geringer Größe umkopiert werden muss (bei gleichen Testdaten), aber das Vergrößern geht auch wesentlich schneller vonstatten.

 

Auffällig ist definitiv auch das die .ToString()-Methode von .NET-StringBuilder keinen zusätzlichen Arbeitsspeicher benötigt im Gegensatz zum .toString()-Äquivalent vom Java-StringBuilder, bei dem vermutlich kopiert wird. Man sollte aber auch bedenken das die Architektur von Java älter ist - insgesamt würde ich die Situation so beurteilen dass man in Java mehr beachten muss um performante Programme zu schreiben.

 

Auch in .NET werden bei der ReadLine-Methode Ein- und Zwei-Zeichen-Zeilenumbrüche gleich behandelt, so dass auch hier das zeilenweise Auslesen im Zusammenhang mit String-Mathing-Algorithmen vermieden werden sollte. In jedem Fall ist das blockweise Einlesen sowohl bei Java als auch beim .NET-Framework am schnellsten – ein Zeilenumbruch beim zeilenweisen Einlesen muss eben auch erst gefunden werden, und das zeichenweise Einlesen ist gar nicht zu empfehlen denn intern wird immer blockweise ausgelesen.

 

Das .NET-Framework arbeitet intern ebenfalls mit 16-Bit-Unicode, daher ist der Speicherverbrauch im RAM bei Dateien mit 8-Bit-Codierung mindestens doppelt so hoch wie auf der Festplatte. Die Dateigröße im RAM in den Testergebnissen ist daher nicht als Byte-Anzahl, sondern als Zeichen-Anzahl zu verstehen.

 

Außerdem aufgefallen war mir das im .NET-Framework die Dateigröße zwar 64-bittig ausgelesen werden kann, der StringBuilder aber nur mit int-Werten als Größenangabe im Konstruktor umgehen kann.

 

Nachfolgend meine Testergebnisse und der Quellcode.

 

Dateigröße auf Festplatte: 65784159

 

1.1) zeilenweises Einlesen mit nötigem Overhead:

Dateigröße im RAM: 657894171

Overhead: 12 Zeichen

00:00:01.3150752 Sekunden Zeit benötigt zum Datei-Laden.

RAM-Verbrauch der ConsoleApplication1.vshost.exe: 140 MB

 

1.2) zeilenweises Einlesen ohne die nötigen 12 Zeichen Overhead: 

Dateigröße im RAM: 657894171

Overhead: 12 Zeichen

00:00:01.5490886 Sekunden Zeit benötigt zum Datei-Laden.

RAM-Verbrauch der ConsoleApplication1.vshost.exe: 270 MB

 

2) zeichenweises Einlesen:

Dateigröße im RAM: 65784159

00:00:04.0932341 Sekunden Zeit benötigt zum Datei-Laden.

RAM-Verbrauch der ConsoleApplication1.vshost.exe: 140 MB

 

3) in 1000-Zeichen-Blöcken einlesen:

Dateigröße im RAM: 65784159

00:00:00.8660495 Sekunden Zeit benötigt zum Datei-Laden.

RAM-Verbrauch der ConsoleApplication1.vshost.exe: 140 MB

 

4) Lesen direkt in String mit ReadAllText-Methode.

Dateigröße im RAM: 65784159

Overhead: 0 Zeichen

00:00:01.0040574 Sekunden Zeit benötigt zum Datei-Laden.

RAM-Verbrauch der ConsoleApplication1.vshost.exe: 270 MB

 

Update 17.01.2012: Analog zu Java gibt es die Append-Methode vom Stringbuilder überladen auch mit den Parametern char-Array, startIndex und charCount - dadurch kann man sich beim blockweisen Lesen den Rest nach der Schleife einsparen:

 

while ((buffer = sR.ReadBlock(ioBuf, 0, 1000)) > 0)
{
  completeString.Append(ioBuf,0,buffer);
}

 

Gemäß MSDN ist es sinnvoller eine Blockgröße von 4096 Bytes für das blockweise Einlesen zu verwenden.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    public class Program
    {
        String test;
      
        public int stringBuilderOverhead = 1000;
        public int fileLength = 0;
        String sep = Environment.NewLine;
        public String loadPath = @"C:\Users\thomas\Downloads\pg2600_2.txt";
        public StringBuilder completeString = new StringBuilder(0);

        /*-----------------------------------------------------------------------------
         *  Variante 1: zeilenweises Einlesen einer Textdatei mit StringBuilder-Overhead
         *-----------------------------------------------------------------------------*/

        void loadFile1()
        {
            int overhead = 0;
            /* nimm die Vorher-Zeit für Gesamtdurchlauf */
            DateTime completeTimeBefore = DateTime.Now;

            completeString = new StringBuilder(fileLength + stringBuilderOverhead);
            try
            {
                String thisLine = null;
                StreamReader sR = new StreamReader(loadPath, System.Text.Encoding.Default);
                while ((thisLine = sR.ReadLine()) != null)
                //while (!sR.EndOfStream)
                {
                    completeString.Append(thisLine);
                    completeString.Append(sep);
                }
                sR.Close();
                completeString.Append(thisLine);
            }
            catch (IOException e)
            {
                Console.WriteLine("Datei konnte nicht eingelesen werden!");
                return;
            }

            Console.WriteLine("Dateigröße im RAM: " + (overhead = completeString.Length));
            Console.WriteLine("Overhead: " + (overhead - fileLength) + " Zeichen");

            /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */
            DateTime completeTimeAfter = DateTime.Now;
            TimeSpan completeTimeDiff = completeTimeAfter - completeTimeBefore;
            Console.WriteLine(completeTimeDiff + " Sekunden Zeit benötigt zum Datei-Laden.\n");
        }

        /*-----------------------------------------------------------------------------
         *  Variante 2: zeichenweises Einlesen und zeichenweises Hinzufügen
         *-----------------------------------------------------------------------------*/

        void loadFile2()
        {
            /* nimm die Vorher-Zeit für Gesamtdurchlauf */
            DateTime completeTimeBefore = DateTime.Now;

            completeString = new StringBuilder(fileLength);
            try
            {
                int thisChar;
                StreamReader sR = new StreamReader(loadPath, System.Text.Encoding.Default);
                while (!sR.EndOfStream)
                {
                    thisChar = sR.Read();
                    completeString.Append((char)thisChar);
                }
                sR.Close();
            }
            catch (IOException e)
            {
                Console.WriteLine("Datei konnte nicht eingelesen werden!");
                return;
            }

            Console.WriteLine("Dateigröße im RAM: " + completeString.Length);

            /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */
            DateTime completeTimeAfter = DateTime.Now;
            TimeSpan completeTimeDiff = completeTimeAfter - completeTimeBefore;
            Console.WriteLine(completeTimeDiff + " Sekunden Zeit benötigt zum Datei-Laden.\n");
        }

        /*-----------------------------------------------------------------------------
         *  Variante 3: in 1000-Zeichen-Blöcken einlesen und hinzufügen
         *-----------------------------------------------------------------------------*/

        void loadFile3()
        {
            /* nimm die Vorher-Zeit für Gesamtdurchlauf */
            DateTime completeTimeBefore = DateTime.Now;

            completeString = new StringBuilder(fileLength);
            try
            {
                char[] ioBuf = new char[1000];
                int buffer = 0;

                StreamReader sR = new StreamReader(loadPath, System.Text.Encoding.Default);
                while ((buffer = sR.ReadBlock(ioBuf, 0, 1000)) == 1000)
                {
                    completeString.Append(ioBuf);
                }
                if (buffer > 0)
                {
                    for (int i = 0; i < buffer; i++)
                        completeString.Append(ioBuf[i]);
                }
                sR.Close();
            }
            catch (IOException e)
            {
                Console.WriteLine("Datei konnte nicht eingelesen werden!");
                return;
            }

            Console.WriteLine("Dateigröße im RAM: " + completeString.Length);

            /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */
            DateTime completeTimeAfter = DateTime.Now;
            TimeSpan completeTimeDiff = completeTimeAfter - completeTimeBefore;
            Console.WriteLine(completeTimeDiff + " Sekunden Zeit benötigt zum Datei-Laden.\n");
        }

        /*-----------------------------------------------------------------------------
         *  Variante 4: Laden mittels ReadAllText-Methode
         *-----------------------------------------------------------------------------*/

        void loadFile4()
        { 
            int overhead=0;
            /* nimm die Vorher-Zeit für Gesamtdurchlauf */
            DateTime completeTimeBefore = DateTime.Now;

            completeString.Append(File.ReadAllText(loadPath, Encoding.Default));
//            test=File.ReadAllText(loadPath, Encoding.Default);

            Console.WriteLine("Dateigröße im RAM: " + (overhead = completeString.Length));
            Console.WriteLine("Overhead: " + (overhead-fileLength) + " Zeichen");

            /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */
            DateTime completeTimeAfter = DateTime.Now;
            TimeSpan completeTimeDiff = completeTimeAfter - completeTimeBefore;
            Console.WriteLine(completeTimeDiff + " Sekunden Zeit benötigt zum Datei-Laden.\n");       
        }

        static void Main(string[] args)
        {
            Program test = new Program();

            FileInfo fInfo = new FileInfo(test.loadPath);
            test.fileLength = (int) fInfo.Length;
            fInfo = null;
            Console.WriteLine("Dateigröße auf Festplatte: " + test.fileLength);

            //Console.WriteLine("\nzeilenweises Einlesen: ");
            //test.loadFile1();

            //Console.WriteLine("\nzeichenweises Einlesen: ");
            //test.loadFile2();

            //Console.WriteLine("\nin 1000-Zeichen-Blöcken einlesen: ");
            //test.loadFile3();
            //test.test = test.completeString.ToString();

            Console.WriteLine("\nLesen direkt in String mit ReadAllText-Methode.");
            test.loadFile4();

            /* Datei zum Diffen schreiben */
            /*StreamWriter f1 = new StreamWriter(@"C:\Users\thomas\Downloads\pg2600_2_2.txt");
            f1.Write(test.completeString);
            f1.Close();*/

            /* damit das Konsolenfenster nicht sofort wieder geschlossen wird */
            Console.ReadKey();

        }
    }
}

 

Monats-Liste