binär code… oder; wenn du schnell sein willst musst du dich selbst darum kümmern.
Im vorherigen Artikel wurden bisher nur Standardserialiserer und ihre Formate miteinander verglichen. Die Ergebnisse waren schon recht ordentlich und die Erkenntnisse aus den Vergleichen sehr lehrreich. Vor allem der Vergleich XML vs. JSON animierte mich dazu, die gewonnenen Erkenntnisse weiter zu führen. Meines Erachtens nach, resultiert der Vorteil von JSON gegenüber XML in der deutlich schlankeren Definition der erzeugten Daten. Während XML vom Klassennamen über die Namen der Enthaltenen Eigenschaften bis zum Typ des Inhalts von Kollektionen alles schreibt, beschränkt sich JSON auf den Namen der Eigenschaften und ihren Inhalt. Wenn ich diesen Gedankengang konsequent weiterführe, kann ich in einem angepassten Serialisierer selbst auf die Namen der Eigenschaften verzichten und statt dessen mittels Indexer auf den jeweiligen Wert des serialisierten Objekts zugreifen.
Als nächste Konsequenz kann die komplette Typbehandlung beim De/Serialisieren entfallen, da der zu de/serialiserende Typ bekannt ist und nur dieser verwendet wird. Aber nun der Reihe nach.

Der Anlass für die Vergleiche der verschiedenen Serialisierer, war eine Anforderung aus einem aktuellen Projekt über das ich in diesem Artikel bereits geschrieben haben:
Eine Kollektion eines bekannten Typs in einem geschlossenen System schnell und effizient zu serialisieren und deserialisieren.
Solange ich einen der Standardserialisierer verwende, werde ich immer die Typbehandlung des jeweiligen Serialisierer in Kauf nehmen müssen, da der Serialisierer mit beinahe jedem gängigen .NET Objekt umgehen muss. Warum also nicht die Serialisierung auf das notwendige beschränken? Das Notwendige ist per Definition alleinig der Inhalt der öffentlichen Eigenschaften.

Wie könnte so eine serialisierte Zeichenfolge aussehen, die den Inhalt der öffentlichen Eigenschaften darstellt?
Da eine Kollektion serialisiert werden soll, muss als erstes zwischen den einzelnen Einträgen der Auflistung getrennt werden. Als nächstes muss eine Trennung zwischen den einzelnen Eigenschaften eines Eintrags her. Am einfachsten erschien mir ein Muster nach folgendem Beispiel:
[#|#|#…]
Die eckigen Klammern umschließen eine Klasse, also einen Eintrag in der Auflistung. Mit einem oder-Operator (|) wird zwischen den einzelnen Eigenschaften abgegrenzt. Zur Darstellung eines Array oder einer anderen Auflistung habe ich mich für folgendes Muster entschieden:
(n,n,n…)
Kombiniert ergibt sich für die Klasse TestNode, aus diesem Artikel, das endgültige Muster:
[#|#|#|(n,n,n…)]

Zum erzeugen der Zeichenfolge, habe ich mich für eine Überladung der ToString-Methode entschlossen, die eine Zeichenfolge als Format erwartet.

internal string ToString(string format)
{
    return string.Format(
        CultureInfo.InvariantCulture,
        "[{0}|{1}|{2}|({3})]",
        this.Name,
        this.MessagingUrl,
        this.Token,
        string.Join(
            ",",
            (this.Ranges
                .Select(i => i.ToString(CultureInfo.InvariantCulture)))
                .ToArray()));
}

Für diesen Benchmark wird der Parameter format noch nicht ausgewertet. Später könnten hier Werte wie etwa “C” für Custom oder “J” für JSON angegeben und entsprechend verarbeitet werden.

Die eigentliche Serialisierung in einem CustomFormatter ist eher trivial. Es wird lediglich die Auflistung durchlaufen, für jedes Element die überladene ToString(string)-Methode aufgerufen und die zurückgegebene Zeichenfolge in einen StreamWriter geschrieben.

public void Serialize(HttpResponse response, List<TestNode> data)
{
    response.ContentType = "application/json";
    using (var writer = new StreamWriter(response.OutputStream))
    {
        foreach (var entry in data)
        {
            writer.Write(entry.ToString(null));
        }
    }
}

Die Deserialisierung ist ähnlich einfach. Der empfangene Stream wird in einen StreamReader gelesen und die enthaltene Zeichenfolge, dem vorher beschriebenen Muster entsprechend, aufgeteilt und die erzeugten Fragmente jeweils einer neuen Instanz der Klasse TestNode bzw. den Eigenschaften zugewiesen.

public List<TestNode> Deserialize(Stream responseStream){
    var list = new List<TestNode>();
    var rawSeparator = new char[] { '[', ']' };
    var propertySeparator = new char[] { '|' };
    var rangeSeparator = new char[] { '(', ')', ',' };
    using (var reader = new StreamReader(responseStream))
    {
        var rawData = reader.ReadToEnd()
            .Split(rawSeparator, StringSplitOptions.RemoveEmptyEntries);
        foreach (var entry in rawData)
        {
            var properties = entry.Split(
                propertySeparator,
                StringSplitOptions.RemoveEmptyEntries);
            if (properties.Length < 1)
            {
                continue;
            }
            var ranges = properties[3]
                .Split(rangeSeparator, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => int.Parse(s, CultureInfo.InvariantCulture));
            var node = new TestNode
            {
                Name = properties[0],
                MessagingUrl = properties[1],
                Token = properties[2],
                Ranges = new List<int>(ranges)
            };
            list.Add(node);
        }
    }
    return list;
}

Die Erzeugung der temporären Variablen ranges, ist nur der Übersicht halber im Code enthalten. Das Aufteilen und parsen der Zeichenfolge kann auch im Konstruktor von List<T> für die Eigenschaft Ranges erfolgen.

Nach dem die Methodik stand wollte ich natürlich wissen, ob auch der erwartete Effekt eintrat bzw. eine verbesserte Leitung gegenüber den bisher verwendeten Serialisierern messbar ist.

Der Benchmark wurde um die Messung der Deserialisierung erweitert. So kann ein besseres Gesamtbild des jeweiligen Serialisierers wiedergegeben werden.
Die Werte der Spalten Response, Deseria. und Complete sind jeweils in Millisekunden dargestellt. Count gibt die Anzahl der Elemente in der übertragenen Auflistung wieder und Transfered die Anzahl der übertragenen Bytes.

Ich habe kurzerhand die beschriebene Methodik als CustomFormatter mit dem Kürzel custom dem Benchmark hinzugefügt und dieses laufen lassen.
Was soll ich sagen; die Zahlen sprechen für sich.

Format Count Comp Response Deseria. Complete Transfered
bin 1
1
gzip 1

1
1
1
968
559
xml 1
1
gzip 1   1 423
364
jsondata 1
1
gzip 1   1 219
269
jsonweb 1
1
gzip 2   2 148
246
jsonnet 1
1
gzip 2
5
  2
5
148
246
custom 1
1
gzip
1
 
1
101
209
Format Count Comp Response Deseria. Complete Transfered
bin 10
10
gzip 1
1
  1
1
2.581
1.372
xml 10
10
gzip 4
1
1 1 3.494
947
jsondata 10
10
gzip 1
1
  1 2.300
813
jsonweb 10
10
gzip 1
1
  2 1.590
773
jsonnet 10
10
gzip 2
1
1
3
1
1.590
773
custom 10
10
gzip 2   2
1.129
723
Format Count Comp Response Deseria. Complete Transfered
bin 100
100
gzip 3
2
29
2
32
4
18.611
8.831
xml 100
100
gzip 1
2
1
1
2
3
33.649
6.312
jsondata 100
100
gzip 1
1
2
3
3
4
23.005
5.713
jsonweb 100
100
gzip 1
2
6
6
7
8
15.905
5.397
jsonnet 100
100
gzip 1
1
2
2
3
3
15.905
5.397
custom 100
100
gzip
1
1
1
1
2
11.304
5.125
Format Count Comp Response Deseria. Complete Transfered
bin 1.000
1.000
gzip 11
20
42
26
53
46
177.927
80.243
xml 1.000
1.000
gzip 5
15
8
10
13
25
329.784
59.224
jsondata 1.000
1.000
gzip 6
13
23
26
29
39
229.068
53.906
jsonweb 1.000
1.000
gzip 11
18
61
65
72
83
158.068
50.626
jsonnet 1.000
1.000
gzip 5
11
19
21
24
32
158.068
50.626
custom 1.000
1.000
gzip 2
8
7
5
9
13
112.067
47.624
Format Count Comp Response Deseria. Complete Transfered
bin 10.000
10.000
gzip 125
210
528
215
653
425
1.770.091
793.997
xml 10.000
10.000
gzip 47
148
114
114
161
262
3.284.950
587.571
jsondata 10.000
10.000
gzip 53
143
377
267
430
410
2.287.996
535.119
jsonweb 10.000
10.000
gzip 113
177
651
711
764
888
1.577.996
502.584
jsonnet 10.000
10.000
gzip 47
112
211
254
258
366
1.577.996
502.584
custom 10.000
10.000
gzip 27
77
48
72
75
149
1.117.995
472.300
Format Count Comp Response Deseria. Complete Transfered
bin 100.000
100.000
gzip 1.389
2.255
11.064
8.115
12.453
10.370
17.701.551
7.887.661
xml 100.000
100.000
gzip 457
1.424
916
1.161
1.373
2.585
32.890.892
5.876.977
jsondata 100.000
100.000
gzip 523
1.300
2.594
2.772
3.117
4.072
22.887.368
5.349.086
jsonweb 100.000
100.000
gzip 1.116
1.781
6.676
6.692
7.792
8.743
15.787.368
5.023.391
jsonnet 100.000
100.000
gzip 462
1.163
2.124
2.261
2.568
3.424
15.787.368
5.023.391
custom 100.000
100.000
gzip 221
842
518
761
739
1.603
11.187.367
4.721.865

Wie obige Zahlen zeigen, schlägt die kleinere Datenmenge sofort zu Buche. Das Fehlen jeglicher Typbehandlung ist in der Responsetime und der sehr kurzen Zeit für die Deserialisierung ebenfalls deutlich zu sehen.

Die vorgestellte Methodik ist beileibe noch kein Produktivcode. Sie soll lediglich zeigen, dass auch einfache Ansätze zu guten Ergebnissen führen können.

Der Beispielcode ist als Projekt SerializeTesting bei Bitbucket gehostet. Erweiterungen um eigene Ideen sind ausdrücklich erwünscht.

Fazit:

Es muss nicht immer ein Serialisierer mit der kompletten Funktionalität verwendet werden.
Wie bereits der Vergleich der gängigen Serialisierer untereinander gezeigt hat, kostet Overhead einfach Zeit. Ob nun das erzeugte Format oder die interne Verarbeitung der zu de/serialisierenden Objekte spielt dabei keine Rolle. Wenn konsequent auf jeglichen Overhead verzichtet wird, ist das daraus resultierende Ergebnis immer schlank und effizient. Wie weit die Entschlackung getrieben wird, ist jedem selbst überlassen.

Technorati-Tags: | | | | |
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitten "kicken" sie ihn.
kick it on dotnet-kicks.de