Bewertung: 5.0 von 8 Benutzern
klaus_b
… 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.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitten "kicken" sie ihn.
