Skip to content

Commit b551224

Browse files
committed
Performance: Faster integer conversion.
Because we know that the numbers we convert are valid integers and dont contain anything except the numericals and a minus sign, we can convert the byte array faster than int.Parse(). Saves some ~10% for both integer and string deserialization, depending on the exact inputs tested. Arrays and objects also benefit because of the length segment they have.
1 parent 0cb8efa commit b551224

File tree

5 files changed

+38
-18
lines changed

5 files changed

+38
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
- Reduced time to decode / re-encode the input string.
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,
18-
doubles and floats.
18+
doubles and booleans.
19+
- Improved performance for implicit deserialization of integers as well as minor improvements for implicit
20+
deserialization of arrays.
1921
- Improved serialization performance for strings, integers, `IList<T>`, `ExpandoObject`, Dictionaries and `PhpDynamicObject`
2022

2123
## Internal

PhpSerializerNET.Test/Deserialize/IntegerDeserialization.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@ public class IntegerDeserializationTest {
1515
[InlineData("i:1;", 1)]
1616
[InlineData("i:2147483647;", int.MaxValue)]
1717
[InlineData("i:-2147483648;", int.MinValue)]
18-
public void DeserializeZero(string input, int expected) {
18+
public void Deserialize(string input, int expected) {
1919
Assert.Equal(expected, PhpSerialization.Deserialize<int>(input));
2020
}
2121

22+
[Theory]
23+
[InlineData("i:0;", 0)]
24+
[InlineData("i:1;", 1)]
25+
[InlineData("i:2147483647;", int.MaxValue)]
26+
[InlineData("i:-2147483648;", int.MinValue)]
27+
public void DeserializeImplicit(string input, long expected) {
28+
Assert.Equal(expected, PhpSerialization.Deserialize(input));
29+
}
30+
2231
[Fact]
2332
public void DeserializeToNullable() {
2433
var result = PhpSerialization.Deserialize<int?>("i:1;");

PhpSerializerNET/Deserialization/PhpDeserializer.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ This Source Code Form is subject to the terms of the Mozilla Public
99
using System.Globalization;
1010
using System.Linq;
1111
using System.Reflection;
12-
using System.Text;
1312

1413
namespace PhpSerializerNET;
1514

1615
internal ref struct PhpDeserializer {
1716
private readonly PhpDeserializationOptions _options;
18-
private Encoding _inputEncoding;
1917
private readonly Span<PhpToken> _tokens;
2018
private readonly ReadOnlySpan<byte> _input;
2119
private int _currentToken = 0;
@@ -24,7 +22,6 @@ internal PhpDeserializer(Span<PhpToken> tokens, ReadOnlySpan<byte> input, PhpDes
2422
_options = options;
2523
_input = input;
2624
_tokens = tokens;
27-
_inputEncoding = _options.InputEncoding;
2825
}
2926

3027
internal object Deserialize() {

PhpSerializerNET/Deserialization/PhpTokenizer.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,11 @@ private ValueSpan GetNumbers() {
4242

4343
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4444
private int GetLength() {
45-
if (this._input[this._position + 1] == ':') {
46-
return _input[_position++] - 48;
45+
int result = 0;
46+
for (; this._input[this._position] != ':'; this._position++) {
47+
result = result * 10 + (this._input[_position] - 48);
4748
}
48-
int start = this._position;
49-
while (this._input[++this._position] != (byte)':') { }
50-
return int.Parse(this._input.Slice(start, this._position - start), CultureInfo.InvariantCulture);
49+
return result;
5150
}
5251

5352
[MethodImpl(MethodImplOptions.AggressiveOptimization)]

PhpSerializerNET/Deserialization/ValueSpan.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace PhpSerializerNET;
1010
using System.Text;
1111

1212
internal readonly struct ValueSpan {
13-
private static ValueSpan _empty = new ValueSpan(0,0);
13+
private static ValueSpan _empty = new ValueSpan(0, 0);
1414
internal readonly int Start;
1515
internal readonly int Length;
1616

@@ -26,19 +26,32 @@ internal ValueSpan(int start, int length) {
2626
internal double GetDouble(ReadOnlySpan<byte> input) {
2727
var value = input.Slice(Start, Length);
2828
return value switch {
29-
[(byte)'I', (byte)'N', (byte)'F'] => double.PositiveInfinity,
30-
[(byte)'-', (byte)'I', (byte)'N', (byte)'F'] => double.NegativeInfinity,
31-
[(byte)'N', (byte)'A', (byte)'N'] => double.NaN,
29+
[(byte)'I', (byte)'N', (byte)'F'] => double.PositiveInfinity,
30+
[(byte)'-', (byte)'I', (byte)'N', (byte)'F'] => double.NegativeInfinity,
31+
[(byte)'N', (byte)'A', (byte)'N'] => double.NaN,
3232
_ => double.Parse(value, CultureInfo.InvariantCulture),
3333
};
3434
}
3535

3636
internal bool GetBool(ReadOnlySpan<byte> input) => input[this.Start] == '1';
3737

38-
internal long GetLong(ReadOnlySpan<byte> input) => long.Parse(
39-
input.Slice(this.Start, this.Length),
40-
CultureInfo.InvariantCulture
41-
);
38+
internal long GetLong(ReadOnlySpan<byte> input) {
39+
// All the PHP integers we deal with here can only be the number characters and an optional "-".
40+
// See also the Validator code.
41+
// 'long.Parse()' has to take into account that we can skip here, making this manual approach faster.
42+
var span = input.Slice(this.Start, this.Length);
43+
int i = 0;
44+
bool isNegative = false;
45+
if (span[0] == '-') {
46+
i++;
47+
isNegative = true;
48+
}
49+
long result = 0;
50+
for (; i < span.Length; i++) {
51+
result = result * 10 + (span[i] - 48);
52+
}
53+
return isNegative ? result * -1 : result;
54+
}
4255

4356
internal string GetString(ReadOnlySpan<byte> input, Encoding inputEncoding) {
4457
return inputEncoding.GetString(input.Slice(this.Start, this.Length));

0 commit comments

Comments
 (0)