Skip to content

Commit b4e281f

Browse files
committed
Eliminate heap allocations in BinaryDecoder/BinaryEncoder hot paths
BinaryDecoder: - DecodeFloat, DecodeDouble, DecodeGuid, ReadIntFixed: replace new byte[] with stackalloc Span<byte>; add Span<byte> overload of ReadAllRequiredBytes - DecodeString: bypass DecodeByteArray, rent from ArrayPool<byte>, decode string, return buffer — avoids a heap allocation per string field BinaryEncoder: - Encode(float): replace BitConverter.GetBytes (allocates) with stackalloc + BitConverter.TryWriteBytes - Encode(Guid): replace ToByteArray() (allocates) with stackalloc + TryWriteBytes - Encode(string): replace Encoding.UTF8.GetBytes(string) (allocates) with ArrayPool<byte> rent, encode into span, delegate to Encode(ReadOnlySpan<byte>)
1 parent fbdd523 commit b4e281f

2 files changed

Lines changed: 73 additions & 24 deletions

File tree

Sources/Core/Microsoft.StreamProcessing/Serializer/Encoders/BinaryDecoder.cs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Licensed under the MIT License
44
// *********************************************************************
55
using System;
6+
using System.Buffers;
67
using System.IO;
78
using System.Runtime.Serialization;
89
using System.Text;
@@ -110,18 +111,15 @@ public ulong DecodeULong()
110111

111112
public float DecodeFloat()
112113
{
113-
var value = new byte[4];
114+
Span<byte> value = stackalloc byte[4];
114115
ReadAllRequiredBytes(value);
115-
if (!BitConverter.IsLittleEndian)
116-
{
117-
Array.Reverse(value);
118-
}
119-
return BitConverter.ToSingle(value, 0);
116+
if (!BitConverter.IsLittleEndian) value.Reverse();
117+
return BitConverter.ToSingle(value);
120118
}
121119

122120
public double DecodeDouble()
123121
{
124-
var value = new byte[8];
122+
Span<byte> value = stackalloc byte[8];
125123
ReadAllRequiredBytes(value);
126124
long longValue = value[0]
127125
| (long)value[1] << 0x8
@@ -142,7 +140,21 @@ public byte[] DecodeByteArray()
142140
return array;
143141
}
144142

145-
public string DecodeString() => Encoding.UTF8.GetString(DecodeByteArray());
143+
public string DecodeString()
144+
{
145+
int byteCount = DecodeInt();
146+
if (byteCount == 0) return string.Empty;
147+
var rented = ArrayPool<byte>.Shared.Rent(byteCount);
148+
try
149+
{
150+
ReadAllRequiredBytes(rented.AsSpan(0, byteCount));
151+
return Encoding.UTF8.GetString(rented, 0, byteCount);
152+
}
153+
finally
154+
{
155+
ArrayPool<byte>.Shared.Return(rented);
156+
}
157+
}
146158

147159
public int DecodeArrayChunk()
148160
{
@@ -157,9 +169,9 @@ public int DecodeArrayChunk()
157169

158170
public Guid DecodeGuid()
159171
{
160-
var array = new byte[16];
161-
ReadAllRequiredBytes(array);
162-
return new Guid(array);
172+
Span<byte> value = stackalloc byte[16];
173+
ReadAllRequiredBytes(value);
174+
return new Guid(value);
163175
}
164176

165177
private void ReadAllRequiredBytes(byte[] array)
@@ -172,15 +184,26 @@ private void ReadAllRequiredBytes(byte[] array)
172184
}
173185
}
174186

187+
private void ReadAllRequiredBytes(Span<byte> span)
188+
{
189+
int totalRead = 0;
190+
while (totalRead < span.Length)
191+
{
192+
int read = this.stream.Read(span.Slice(totalRead));
193+
if (read == 0)
194+
throw new SerializationException($"Unexpected end of stream: '{span.Length - totalRead}' bytes missing.");
195+
totalRead += read;
196+
}
197+
}
198+
175199
private int ReadIntFixed()
176200
{
177-
var value = new byte[4];
178-
this.stream.ReadAllRequiredBytes(value, 0, value.Length);
179-
int intValue = value[0]
201+
Span<byte> value = stackalloc byte[4];
202+
ReadAllRequiredBytes(value);
203+
return value[0]
180204
| value[1] << 0x8
181205
| value[2] << 0x10
182206
| value[3] << 0x18;
183-
return intValue;
184207
}
185208

186209
public unsafe T[] DecodeArray<T>() where T : struct

Sources/Core/Microsoft.StreamProcessing/Serializer/Encoders/BinaryEncoder.cs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Licensed under the MIT License
44
// *********************************************************************
55
using System;
6+
using System.Buffers;
67
using System.IO;
78
using System.Text;
89
using Microsoft.StreamProcessing.Internal;
@@ -83,13 +84,10 @@ public void Encode(ulong value)
8384

8485
public void Encode(float value)
8586
{
86-
byte[] bytes = BitConverter.GetBytes(value);
87-
if (!BitConverter.IsLittleEndian)
88-
{
89-
Array.Reverse(bytes);
90-
}
91-
92-
this.stream.Write(bytes, 0, bytes.Length);
87+
Span<byte> bytes = stackalloc byte[4];
88+
BitConverter.TryWriteBytes(bytes, value);
89+
if (!BitConverter.IsLittleEndian) bytes.Reverse();
90+
this.stream.Write(bytes);
9391
}
9492

9593
public void Encode(double value)
@@ -113,12 +111,40 @@ public void Encode(byte[] value)
113111
if (value.Length > 0) this.stream.Write(value, 0, value.Length);
114112
}
115113

114+
public void Encode(ReadOnlySpan<byte> span)
115+
{
116+
int count = span.Length;
117+
Encode(count);
118+
if (count > 0)
119+
{
120+
this.stream.Write(span);
121+
}
122+
}
123+
116124
public void Encode(string value)
117-
=> Encode(Encoding.UTF8.GetBytes(value ?? throw new ArgumentNullException(nameof(value))));
125+
{
126+
if (value == null) throw new ArgumentNullException(nameof(value));
127+
int byteCount = Encoding.UTF8.GetByteCount(value);
128+
var rented = ArrayPool<byte>.Shared.Rent(byteCount);
129+
try
130+
{
131+
int written = Encoding.UTF8.GetBytes(value, rented);
132+
Encode(rented.AsSpan(0, written));
133+
}
134+
finally
135+
{
136+
ArrayPool<byte>.Shared.Return(rented);
137+
}
138+
}
118139

119140
public void EncodeArrayChunk(int size) => Encode(size);
120141

121-
public void Encode(Guid value) => this.stream.Write(value.ToByteArray(), 0, 16);
142+
public void Encode(Guid value)
143+
{
144+
Span<byte> bytes = stackalloc byte[16];
145+
value.TryWriteBytes(bytes);
146+
this.stream.Write(bytes);
147+
}
122148

123149
private void WriteIntFixed(int encodedValue)
124150
{

0 commit comments

Comments
 (0)