Skip to content

Commit 99fe53e

Browse files
committed
Serialization: Improve performance by foregoing StringBuilder in most cases.
Using an array and string.Concat can reduce heap allocation and is always faster when the number strings is known but the total length of the output is not. Serializing a string array with 12 "Hello World"s, this commit improves the time from 1.35µs to 0.72µs.
1 parent 14c51c4 commit 99fe53e

File tree

3 files changed

+75
-90
lines changed

3 files changed

+75
-90
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Reduced memory allocations both in the input re-encoding and the deserialization.
1717
- Delay the materialization of strings when deserializing. This can avoid string allocations entirely for integers,
1818
doubles and floats.
19+
- Improved serialization performance for strings, integers, `IList<T>`, `ExpandoObject`, Dictionaries and `PhpDynamicObject`
1920

2021
## Internal
2122
Split the deserialization into 3 phases:

PhpSerializerNET/PhpSerializer.cs

Lines changed: 73 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,49 @@ internal class PhpSerializer {
2121

2222
public PhpSerializer(PhpSerializiationOptions options = null) {
2323
this._options = options ?? PhpSerializiationOptions.DefaultOptions;
24-
2524
this._seenObjects = new();
2625
}
2726

2827
public string Serialize(object input) {
2928
switch (input) {
30-
case Enum enumValue: {
31-
if (this._options.NumericEnums) {
32-
return $"i:{enumValue.GetNumericString()};";
33-
} else {
34-
return this.Serialize(enumValue.ToString());
35-
}
36-
}
37-
case long longValue: {
38-
return $"i:{longValue};";
39-
}
40-
case int integerValue: {
41-
return $"i:{integerValue};";
42-
}
43-
case double floatValue: {
44-
if (double.IsPositiveInfinity(floatValue)) {
45-
return $"d:INF;";
46-
}
47-
if (double.IsNegativeInfinity(floatValue)) {
48-
return $"d:-INF;";
49-
}
50-
if (double.IsNaN(floatValue)) {
51-
return $"d:NAN;";
52-
}
53-
return $"d:{floatValue.ToString(CultureInfo.InvariantCulture)};";
54-
}
55-
case string stringValue: {
56-
// Use the UTF8 byte count, because that's what the PHP implementation does:
57-
return $"s:{ASCIIEncoding.UTF8.GetByteCount(stringValue)}:\"{stringValue}\";";
29+
case Enum enumValue:
30+
if (this._options.NumericEnums) {
31+
return $"i:{enumValue.GetNumericString()};";
32+
} else {
33+
return this.Serialize(enumValue.ToString());
5834
}
59-
case bool boolValue: {
60-
return boolValue ? "b:1;" : "b:0;";
35+
36+
case long longValue:
37+
return string.Concat("i:", longValue.ToString(CultureInfo.InvariantCulture), ";");
38+
39+
case int integerValue:
40+
return string.Concat("i:", integerValue.ToString(CultureInfo.InvariantCulture), ";");
41+
42+
case double floatValue:
43+
if (double.IsPositiveInfinity(floatValue)) {
44+
return $"d:INF;";
6145
}
62-
case null: {
63-
return "N;";
46+
if (double.IsNegativeInfinity(floatValue)) {
47+
return $"d:-INF;";
6448
}
65-
default: {
66-
return this.SerializeComplex(input);
49+
if (double.IsNaN(floatValue)) {
50+
return $"d:NAN;";
6751
}
52+
return string.Concat("d:", floatValue.ToString(CultureInfo.InvariantCulture), ";");
53+
54+
case string stringValue:
55+
// Use the UTF8 byte count, because that's what the PHP implementation does:
56+
string length = Encoding.UTF8.GetByteCount(stringValue).ToString(CultureInfo.InvariantCulture);
57+
return string.Concat("s:", length, ":\"", stringValue, "\";");
58+
59+
case bool boolValue:
60+
return boolValue ? "b:1;" : "b:0;";
61+
62+
case null:
63+
return "N;";
64+
65+
default:
66+
return this.SerializeComplex(input);
6867
}
6968
}
7069

@@ -77,59 +76,42 @@ private string SerializeComplex(object input) {
7776
}
7877
this._seenObjects.Add(input);
7978

80-
StringBuilder output = new StringBuilder();
8179
switch (input) {
8280
case PhpDynamicObject dynamicObject: {
83-
var inputType = input.GetType();
8481
var className = dynamicObject.GetClassName() ?? "stdClass";
85-
IEnumerable<string> memberNames = dynamicObject.GetDynamicMemberNames();
86-
87-
output.Append("O:")
88-
.Append(className.Length)
89-
.Append(":\"")
90-
.Append(className)
91-
.Append("\":")
92-
.Append(memberNames.Count())
93-
.Append(":{");
94-
95-
foreach (string memberName in memberNames) {
96-
output.Append(this.Serialize(memberName))
97-
.Append(this.Serialize(dynamicObject.GetMember(memberName)));
82+
ICollection<string> memberNames = dynamicObject.GetDynamicMemberNames();
83+
string preamble = $"O:{className.Length}:\"{className}\":{memberNames.Count}:{{";
84+
string[] entryStrings = new string[memberNames.Count * 2];
85+
int entryIndex = 0;
86+
foreach (var memberName in memberNames) {
87+
entryStrings[entryIndex] = this.Serialize(memberName);
88+
entryStrings[entryIndex + 1] = this.Serialize(dynamicObject.GetMember(memberName));
89+
entryIndex += 2;
9890
}
99-
output.Append('}');
100-
return output.ToString();
91+
return string.Concat(preamble, string.Concat(entryStrings), "}");
10192
}
10293
case ExpandoObject expando: {
10394
var dictionary = (IDictionary<string, object>)expando;
104-
var inputType = input.GetType();
105-
output.Append("O:")
106-
.Append("stdClass".Length)
107-
.Append(":\"")
108-
.Append("stdClass")
109-
.Append("\":")
110-
.Append(dictionary.Keys.Count)
111-
.Append(":{");
112-
113-
foreach (var keyValue in dictionary) {
114-
output.Append(this.Serialize(keyValue.Key))
115-
.Append(this.Serialize(keyValue.Value));
95+
string preamble = $"O:8:\"stdClass\":{dictionary.Keys.Count}:{{";
96+
97+
string[] entryStrings = new string[dictionary.Count * 2];
98+
int entryIndex = 0;
99+
foreach (var entry in dictionary) {
100+
entryStrings[entryIndex] = this.Serialize(entry.Key);
101+
entryStrings[entryIndex + 1] = this.Serialize(entry.Value);
102+
entryIndex += 2;
116103
}
117-
output.Append('}');
118-
return output.ToString();
104+
return string.Concat(preamble, string.Concat(entryStrings), "}");
119105
}
120106
case IDynamicMetaObjectProvider:
121107
throw new NotSupportedException(
122108
"Serialization support for dynamic objects is limited to PhpSerializerNET.PhpDynamicObject and System.Dynamic.ExpandoObject in this version."
123109
);
124110
case IDictionary dictionary: {
111+
string preamble;
125112
if (input is IPhpObject phpObject) {
126-
output.Append("O:");
127-
output.Append(phpObject.GetClassName().Length);
128-
output.Append(":\"");
129-
output.Append(phpObject.GetClassName());
130-
output.Append("\":");
131-
output.Append(dictionary.Count);
132-
output.Append(":{");
113+
string className = phpObject.GetClassName();
114+
preamble = $"O:{className.Length}:\"{className}\":{dictionary.Count}:{{";
133115
} else {
134116
var dictionaryType = dictionary.GetType();
135117
if (dictionaryType.GenericTypeArguments.Length > 0) {
@@ -138,28 +120,31 @@ private string SerializeComplex(object input) {
138120
throw new Exception($"Can not serialize into associative array with key type {keyType.FullName}");
139121
}
140122
}
141-
142-
output.Append($"a:{dictionary.Count}:");
143-
output.Append('{');
123+
preamble = $"a:{dictionary.Count}:{{";
144124
}
145125

126+
string[] entryStrings = new string[dictionary.Count * 2];
127+
int entryIndex = 0;
146128
foreach (DictionaryEntry entry in dictionary) {
147-
output.Append($"{this.Serialize(entry.Key)}{this.Serialize(entry.Value)}");
129+
entryStrings[entryIndex] = this.Serialize(entry.Key);
130+
entryStrings[entryIndex + 1] = this.Serialize(entry.Value);
131+
entryIndex += 2;
148132
}
149-
output.Append('}');
150-
return output.ToString();
133+
return string.Concat(preamble, string.Concat(entryStrings), "}");
151134
}
152-
case IList collection: {
153-
output.Append($"a:{collection.Count}:");
154-
output.Append('{');
155-
for (int i = 0; i < collection.Count; i++) {
156-
output.Append(this.Serialize(i));
157-
output.Append(this.Serialize(collection[i]));
158-
}
159-
output.Append('}');
160-
return output.ToString();
135+
case IList collection:
136+
string[] itemStrings = new string[collection.Count * 2];
137+
for (int i = 0; i < itemStrings.Length; i += 2) {
138+
itemStrings[i] = string.Concat("i:", (i / 2).ToString(CultureInfo.InvariantCulture), ";");
139+
itemStrings[i + 1] = this.Serialize(collection[i / 2]);
161140
}
141+
return string.Concat(
142+
$"a:{collection.Count}:{{",
143+
string.Concat(itemStrings),
144+
"}"
145+
);
162146
default: {
147+
StringBuilder output = new StringBuilder();
163148
var inputType = input.GetType();
164149

165150
if (typeof(IPhpObject).IsAssignableFrom(inputType) || inputType.GetCustomAttribute<PhpClass>() != null) {
@@ -190,7 +175,6 @@ private string SerializeComplex(object input) {
190175
}
191176
}
192177
}
193-
194178
output.Append($"a:{members.Count}:");
195179
output.Append('{');
196180
foreach (var member in members) {

PhpSerializerNET/Types/PhpDynamicObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ internal object GetMember(string name) {
2323
return this._dictionary[name];
2424
}
2525

26-
public override IEnumerable<string> GetDynamicMemberNames() {
26+
public override ICollection<string> GetDynamicMemberNames() {
2727
return this._dictionary.Keys;
2828
}
2929

0 commit comments

Comments
 (0)