Posts mit dem Label performance werden angezeigt. Alle Posts anzeigen
Posts mit dem Label performance werden angezeigt. Alle Posts anzeigen

Samstag, 13. Oktober 2012

Große XML Datei in mehrere kleine Dateien aufteilen

Problembeschreibung


Wer viel mit XML-Dateien zu tun hat, kennt vielleicht das Problem - man hat gerade im Programm (oder aus einer Datenbank) ein XML erzeugt, das witzige 50 MB groß ist. Eigentlich in der heutigen Zeit von Terabyte-Festplatten kein großer Wert, sollte man meinen, aber Visual Studio hängt sich beim Öffnen auf. Auch Notepad++ hat hier selbst mit SSD bedenkliche Probleme. Einzig das gute alte Notepad von Windows kann die Datei zumindest anzeigen - das war's aber dann auch, denn damit kann ich nicht wirklich viel anfangen, immerhin sind 50MB auf einer Zeile nicht wirklich übersichtlich.

Lösung

Ich habe mir ein kleines Programm geschrieben, dass ein Eingabe XML in mehrere kleinere XML aufteilt. Die Herausforderung lag darin die Datei nicht komplett einzulesen, sondern Knoten für Knoten zu verarbeiten. Außerdem musste sich das Porgramm merken, welche Knoten noch geöffnet sind, um diese dann zu schließen, sobald die maximale Anzahl an Bytes erreicht wurde. Dadurch ist gewährleistet, dass die resultierenden XML-Dateien auch valides XML repräsentieren.
 

Implementierung

Im Prinzip verwende ich für das Lesen der XML-Datei einen XmlTextReader , das hat den entscheidenden Vorteil, dass die XML-Elemente einzeln eingelesen werden und man XML spezifische Metainformationen zu jedem gelesenen Element erhält. Ich spar mir im Code das kopieren der Kommentare und der XML-Deklaration, wenn das jemand benötigt, ist das schnell zu ergänzen. Außerdem verzichte ich auf die Berücksichtigung von Namespaces.
 
 Der  Programmablauf ist recht einfach:
- Lese jeden Tag und puffere diesen
- öffnende Tags werden auf einen Stack gelegt
- bei schließenden Tags wird der letzte öffnende Tag vom Stack entfernt
- hat der Puffer die gewünschte Größe, so schließe alle noch offenen Tags und schreibe den Puffer auf Platte
- leere Puffer
- öffne die zuletzt offenen Tags wieder im aktuellen Puffer
 
Und hier nun der Code:
 
 
    class XmlUtility
    {
        #region XmlOpeningTag class
        class XmlOpeningTag
        {
            
            public XmlOpeningTag(string name, string fullOpeningTag)
            {
                TagName = name;
                FullOpeningTag = fullOpeningTag;
            }
            public string TagName { get; set; }
            public string FullOpeningTag { get; set; }
            public int ClosingTagSize
            {
                get 
                {
                    return TagName.Length + 3;
                }
            }
        }
        #endregion
        
        /// <summary>
        /// splits the given xml file into smaller parts
        /// </summary>
        /// <param name="pathToXmlFile">the path to the xml file, that is to be splitted</param>
        /// <param name="maxSizeInBytes">app. maximum size of the resulting parts</param>
        /// <param name="resultPath">where to write (path) the resulting files</param>
        /// <returns>list of filepaths, that were created</returns>
        public static List<string> Split(string pathToXmlFile, int maxSizeInBytes, string resultPath=null)
        {
            if (resultPath == null)
                resultPath=System.IO.Path.GetDirectoryName(pathToXmlFile);
            List<string> res = new List<string>();
            // initialize variables
            int currentFileNumber = 1;
            int sizeOfClosingTags = 0;
            string fileWithoutExtension = System.IO.Path.Combine(
                resultPath,               
                System.IO.Path.GetFileNameWithoutExtension(pathToXmlFile));
            StringBuilder output = new StringBuilder();
            Stack<XmlOpeningTag> _openElements = new Stack<XmlOpeningTag>();

            // open the xml file within a xmlreader
            System.Xml.XmlTextReader xmlReader = new System.Xml.XmlTextReader(pathToXmlFile);
            // read from the file, element by element
            while (xmlReader.Read())
            {
                string currentXmlLine = string.Empty;
                XmlOpeningTag newOpeningTag = null;
                switch (xmlReader.NodeType)
                {
                    case XmlNodeType.Element: //its an xml tag element (opening)
                        string xmlElementName = xmlReader.Name;
                        // start with the tag
                        currentXmlLine = "<" + xmlElementName;
                        bool isEmptyElement = xmlReader.IsEmptyElement;
                        // add all attributes
                        while (xmlReader.MoveToNextAttribute()) 
                            currentXmlLine += " " + xmlReader.Name + "='" + xmlReader.Value + "'";
                        // if the element is empty, close it in line
                        if (isEmptyElement )
                            currentXmlLine += "/>";
                        //otherwise just close the tag
                        else
                        {
                            currentXmlLine += ">";
                            newOpeningTag =
                             new XmlOpeningTag(
                                    xmlElementName,
                                    currentXmlLine
                                    );
                            sizeOfClosingTags += newOpeningTag.ClosingTagSize;
                        }
                        break;
                    case XmlNodeType.Text: // text node
                        currentXmlLine = xmlReader.Value;
                        break;
                    case XmlNodeType.EndElement: //end of element tag
                       // pop the last opening tag
                       XmlOpeningTag openingTag = _openElements.Pop();
                       sizeOfClosingTags -= openingTag.ClosingTagSize;
                       currentXmlLine = "</" + xmlReader.Name + ">";
                       break;
                }
                
                // lets see, if we can add this new line
                if (output.Length + sizeOfClosingTags + currentXmlLine.Length >= maxSizeInBytes
                    && xmlReader.NodeType != XmlNodeType.EndElement 
                    // endelements will be added anyways, we cant close the xml 
                    // before adding the endelement
                    )
                {
                    // na we cannot
                    // close all open tags
                    Stack<XmlOpeningTag> helperStack = new Stack<XmlOpeningTag>();
                    while (_openElements.Count > 0)
                    {
                        XmlOpeningTag ol = _openElements.Pop();
                        output.Append("</" + ol.TagName + ">");
                        helperStack.Push(ol);
                    }
                    
                    string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                    res.Add(filePathToSave);
                    System.IO.File.WriteAllText(filePathToSave, output.ToString());
                    currentFileNumber++;
                    // clear the string builder, the old stuff was saved
                    output.Clear();
                    // open the "old open" tags in the new output
                    while (helperStack.Count > 0)
                    {
                        XmlOpeningTag ol = helperStack.Pop();
                        output.Append(ol.FullOpeningTag);
                        _openElements.Push(ol);
                    }

                }

                if (newOpeningTag != null)
                    _openElements.Push(newOpeningTag);
                output.Append(currentXmlLine);
            }
            if (output.Length > 0)
            {
                string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                res.Add(filePathToSave);
                System.IO.File.WriteAllText(filePathToSave, output.ToString());
                currentFileNumber++;
            }
            
            xmlReader.Close();
            return res;
        }
    }

Montag, 11. Juni 2012

Dictionary oder Hashtable

Motivation

In einigen Programmiersprachen ist der Einsatz von Hashtabellen (oder auch assoziativen Arrays) Gang und Gäbe. So zum Beispiel in Perl oder PHP. In C# existiert diese Datenstruktur selbstverständlich auch, jedoch existiert eine generisch typisierte Alternative zu den "normalen" Hashtabellen, das Dictionary.
Doch welche dieser beiden Implementierungen ist vorzuziehen?
Die msdn-Dokumentation (msdn: Hashtable-Auflistungstyp und Dictionary-Auflistungstyp) gibt hierzu leider nur wenig Auskunft, lediglich bei Werttypen soll ein Dictionary leistungsfähiger sein. Der Artikel legt die Vermutung nahe, dass bis auf die Typisierung und ein wenig syntaktischer Zucker (wie TryGetValue) beide Implementierungen ziemlich identisch sind.


Was ist eigentlich eine Hashtabelle?

Eine Hashtabelle wird oft mit Eimern (Buckets) verglichen. Da ich viel bei einem schwedischen Möbelhaus einkaufe, würde ich eher einen Vergleich mit dem Ikea-Lager vorziehen. Grundsätzlich besteht eine Hashtabelle immer aus Schlüssel-Wert-Paaren, anhand eines Schlüssels kann man in einer Hashtabelle ziemlich schnell (konstante Zeit) einen Wert nachschlagen.

Kommen wir zu dem Vergleich mit dem Möbellager, hier wäre der Schlüssel das aufgebaute Möbel, so wie wir es in der Möbelausstellung sehen. Der Wert, den wir jedoch im Möbellager erhalten wollen (oder sollen), ist der zugehörige Bausatz. Im Möbellager müssen wir nun, nachdem wir uns für ein bestimmtes Möbel entschieden haben, den zugehörigen Bausatz suchen, sondern verwenden eine Lagerplatznummer. Dadurch wird jedem Möbel ein eindeutiger Lagerplatz zugewiesen. In der Informatik ist diese Abbildung die Hashfunktion, bei Ikea wird es dafür keine mathematische Formel geben, sondern vielmehr ein Fakturierungsprogramm, dass die Abbildung durchführt.
                    f: Möbel -> Lagerplatz

Testparameter

Um diesen Test durchzuführen, habe ich ein kleines Programm geschrieben, welches Keys und Values jeweils als Strings variabler Länge erzeugt (zwischen 10 und 100 Zeichen). Bei der Suche betrachte ich 2 Szenarien, zum einen die Option, dass jeder Suchschlüssel auch in der Hashtable vorkommt, und zum Anderen, die Option, die wohl am Häufigsten in der Paxis auftaucht: Es existieren etwa 50% der Schlüssel, nach denen gesucht wird.

Test 1: Einfügen von Schlüssel-Wert-Paaren

Beim Einfügen gibt es einen klaren Sieger: das Dictionary, bei großen Datensätzen (ca. 1.000.000) ergibt sich ein Vorteil von ca. 35 %, bei kleineren Tabellen, soger ein Vorteil von bis zu 50 %.

Test 2: Suchen von Schlüsseln

Hier ist das Ergebnis nicht ganz klar, bei großen Datensätzen gewinnt hier die Hashtable mit knapp 6 %. Bei kleinen Testmengen liegt der Vorteil jedoch bei dem Dictionary (ca. 8 %)

Während sich die Nachschlagezeiten in der Hashtable nach vorhandenen und nicht vorhandenen Schlüsseln die Waage hält, ist das Dictionary sogar noch um ca. 3 % schneller, wenn die Hälfte der Schlüssel nicht im Dictionary enthalten sind.

Tipp

Bei der Verwendung des Dictionary ist mit aufgefallen, dass es nur einen Vorteil gibt, wenn man anstatt des vorherigen Prüfens (mittels ContainsKey) mit der Funktion TryGetValue arbeitet:
if (testDictionary.ContainsKey(keyToFind))
    valueFound = testDictionary[keyToFind];

// Besser:

string valueFound = string.Empty;
testDictionary.TryGetValue(keyToFind, out valueFound);


Fazit

Nimmt man die Testergebnisse her, so überwiegt der <u>Vorteil bei dem Dictionary</u>. Dem zugegebener Maßen geringen Performance-Vorteile der Hashtable beim Suchen in großen Datensätzen, steht ein eindeutig besserer Programmierstil, durch die Verwendung von Dictionaries, gegenüber.<br /> <br /></body>

Samstag, 9. Juni 2012

VisualStudio Performance Wizard mit signierten Assemblies

Grundsätzlich kann es beim Verwenden des VisualStudio Profiler zu Problemen kommen, wenn man mit signierten Assemblies arbeitet. Beim Abschalten der Signierung für die gerade zu überwachende Library kann es jedoch zu Problemen kommen, wenn die abhängigen, signierten Assemblies geladen werden. Daher empfiehlt es sich beim Profiling die Signatur komplett abzuschalten.


Abschalten der Assembly-Signierung im VisualStudio Profiler

Rechte MT auf die Performance-Instanz > Eigenschaften und dort als Post-Instrument Event folgendes angeben:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\sn.exe"  -Vr *