Skip to content

Commit f0bece5

Browse files
committed
CSHARP-2693/CSHARP-2692: Encoding.GetBytes buffer caching optimization
1 parent a9bbf5e commit f0bece5

File tree

11 files changed

+605
-80
lines changed

11 files changed

+605
-80
lines changed

src/MongoDB.Bson/IO/BsonStreamAdapter.cs

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
using System;
1717
using System.IO;
18-
using System.Linq;
1918
using System.Text;
2019
using System.Threading;
2120
using System.Threading.Tasks;
@@ -32,7 +31,6 @@ public sealed class BsonStreamAdapter : BsonStream
3231
private bool _ownsStream;
3332
private readonly Stream _stream;
3433
private readonly byte[] _temp = new byte[12];
35-
private readonly byte[] _tempUtf8 = new byte[128];
3634

3735
// constructors
3836
/// <summary>
@@ -52,7 +50,7 @@ public BsonStreamAdapter(Stream stream, bool ownsStream = false)
5250
_ownsStream = ownsStream;
5351
}
5452

55-
// properties
53+
// properties
5654
/// <summary>
5755
/// Gets the base stream.
5856
/// </summary>
@@ -372,7 +370,9 @@ public override string ReadString(UTF8Encoding encoding)
372370
ThrowIfDisposed();
373371

374372
var length = ReadInt32();
375-
var bytes = length <= _tempUtf8.Length ? _tempUtf8 : new byte[length];
373+
using var rentedBuffer = ThreadStaticBuffer.RentBuffer(length);
374+
var bytes = rentedBuffer.Bytes;
375+
376376
this.ReadBytes(bytes, 0, length);
377377
if (bytes[length - 1] != 0)
378378
{
@@ -454,26 +454,33 @@ public override void WriteCString(string value)
454454
}
455455
ThrowIfDisposed();
456456

457-
byte[] bytes;
458-
int length;
459457

460-
if (CStringUtf8Encoding.GetMaxByteCount(value.Length) <= _tempUtf8.Length)
458+
// Compare to 128 to preserve original behavior
459+
const int maxLengthToUseCStringUtf8EncodingWith = 128;
460+
if (CStringUtf8Encoding.GetMaxByteCount(value.Length) <= maxLengthToUseCStringUtf8EncodingWith)
461461
{
462-
bytes = _tempUtf8;
463-
length = CStringUtf8Encoding.GetBytes(value, _tempUtf8, 0, Utf8Encodings.Strict);
462+
using var rentedBuffer = ThreadStaticBuffer.RentBuffer(maxLengthToUseCStringUtf8EncodingWith);
463+
464+
var length = CStringUtf8Encoding.GetBytes(value, rentedBuffer.Bytes, 0, Utf8Encodings.Strict);
465+
WriteBytes(rentedBuffer.Bytes, length);
464466
}
465467
else
466468
{
467-
bytes = Utf8Encodings.Strict.GetBytes(value);
468-
if (Array.IndexOf<byte>(bytes, 0) != -1)
469+
using var rentedSegment = Utf8Encodings.Strict.GetBytesUsingThreadStaticBuffer(value);
470+
var segment = rentedSegment.Segment;
471+
if (Array.IndexOf<byte>(segment.Array, 0, 0, segment.Count) != -1)
469472
{
470473
throw new ArgumentException("A CString cannot contain null bytes.", "value");
471474
}
472-
length = bytes.Length;
475+
476+
WriteBytes(segment.Array, segment.Count);
473477
}
474478

475-
_stream.Write(bytes, 0, length);
476-
_stream.WriteByte(0);
479+
void WriteBytes(byte[] bytes, int length)
480+
{
481+
_stream.Write(bytes, 0, length);
482+
_stream.WriteByte(0);
483+
}
477484
}
478485

479486
/// <inheritdoc/>
@@ -545,22 +552,11 @@ public override void WriteString(string value, UTF8Encoding encoding)
545552
}
546553
ThrowIfDisposed();
547554

548-
byte[] bytes;
549-
int length;
550-
551-
if (encoding.GetMaxByteCount(value.Length) <= _tempUtf8.Length)
552-
{
553-
bytes = _tempUtf8;
554-
length = encoding.GetBytes(value, 0, value.Length, _tempUtf8, 0);
555-
}
556-
else
557-
{
558-
bytes = encoding.GetBytes(value);
559-
length = bytes.Length;
560-
}
555+
using var rentedSegment = encoding.GetBytesUsingThreadStaticBuffer(value);
556+
var segment = rentedSegment.Segment;
561557

562-
WriteInt32(length + 1);
563-
_stream.Write(bytes, 0, length);
558+
WriteInt32(segment.Count + 1);
559+
_stream.Write(segment.Array, 0, segment.Count);
564560
_stream.WriteByte(0);
565561
}
566562
}

src/MongoDB.Bson/IO/BsonTrie.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.IO;
18-
using System.Text;
1917

2018
namespace MongoDB.Bson.IO
2119
{
@@ -57,10 +55,10 @@ public BsonTrieNode<TValue> Root
5755
/// <param name="value">The value to add. The value can be null for reference types.</param>
5856
public void Add(string elementName, TValue value)
5957
{
60-
var utf8 = Utf8Encodings.Strict.GetBytes(elementName);
58+
using var rentedSegment = Utf8Encodings.Strict.GetBytesUsingThreadStaticBuffer(elementName);
6159

6260
var node = _root;
63-
foreach (var keyByte in utf8)
61+
foreach (var keyByte in rentedSegment.Segment)
6462
{
6563
var child = node.GetChild(keyByte);
6664
if (child == null)
@@ -80,7 +78,7 @@ public void Add(string elementName, TValue value)
8078
/// <param name="utf8">The element name.</param>
8179
/// <param name="node">
8280
/// When this method returns, contains the node associated with the specified element name, if the key is found;
83-
/// otherwise, null. This parameter is passed unitialized.
81+
/// otherwise, null. This parameter is passed uninitialized.
8482
/// </param>
8583
/// <returns>True if the node was found; otherwise, false.</returns>
8684
public bool TryGetNode(ArraySegment<byte> utf8, out BsonTrieNode<TValue> node)
@@ -125,7 +123,7 @@ public bool TryGetNode(BsonStream stream, out BsonTrieNode<TValue> node)
125123
/// <param name="utf8">The element name.</param>
126124
/// <param name="value">
127125
/// When this method returns, contains the value associated with the specified element name, if the key is found;
128-
/// otherwise, the default value for the type of the value parameter. This parameter is passed unitialized.
126+
/// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.
129127
/// </param>
130128
/// <returns>True if the value was found; otherwise, false.</returns>
131129
public bool TryGetValue(ArraySegment<byte> utf8, out TValue value)
@@ -147,14 +145,13 @@ public bool TryGetValue(ArraySegment<byte> utf8, out TValue value)
147145
/// <param name="elementName">The element name.</param>
148146
/// <param name="value">
149147
/// When this method returns, contains the value associated with the specified element name, if the key is found;
150-
/// otherwise, the default value for the type of the value parameter. This parameter is passed unitialized.
148+
/// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.
151149
/// </param>
152150
/// <returns>True if the value was found; otherwise, false.</returns>
153151
public bool TryGetValue(string elementName, out TValue value)
154152
{
155-
var bytes = Utf8Encodings.Strict.GetBytes(elementName);
156-
var utf8 = new ArraySegment<byte>(bytes, 0, bytes.Length);
157-
return TryGetValue(utf8, out value);
153+
using var utf8 = Utf8Encodings.Strict.GetBytesUsingThreadStaticBuffer(elementName);
154+
return TryGetValue(utf8.Segment, out value);
158155
}
159156
}
160157

src/MongoDB.Bson/IO/ByteBufferStream.cs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public class ByteBufferStream : BsonStream, IStreamEfficientCopyTo
3232
private readonly bool _ownsBuffer;
3333
private int _position;
3434
private readonly byte[] _temp = new byte[12];
35-
private readonly byte[] _tempUtf8 = new byte[128];
3635

3736
// constructors
3837
/// <summary>
@@ -529,12 +528,14 @@ public override string ReadString(UTF8Encoding encoding)
529528
}
530529
else
531530
{
532-
var bytes = length <= _tempUtf8.Length ? _tempUtf8 : new byte[length];
531+
using var rentedBuffer = ThreadStaticBuffer.RentBuffer(length);
532+
var bytes = rentedBuffer.Bytes;
533533
this.ReadBytes(bytes, 0, length);
534534
if (bytes[length - 1] != 0)
535535
{
536536
throw new FormatException("String is missing terminating null byte.");
537537
}
538+
538539
return Utf8Helper.DecodeUtf8String(bytes, 0, length - 1, encoding);
539540
}
540541
}
@@ -568,24 +569,35 @@ public override void WriteCString(string value)
568569
}
569570
else
570571
{
571-
byte[] bytes;
572-
if (maxLength <= _tempUtf8.Length)
572+
// Compare to 128 to preserve original behavior
573+
const int maxLengthToUseCStringUtf8EncodingWith = 128;
574+
575+
if (maxLength <= maxLengthToUseCStringUtf8EncodingWith)
573576
{
574-
bytes = _tempUtf8;
575-
actualLength = CStringUtf8Encoding.GetBytes(value, bytes, 0, Utf8Encodings.Strict);
577+
using var rentedBuffer = ThreadStaticBuffer.RentBuffer(maxLengthToUseCStringUtf8EncodingWith);
578+
actualLength = CStringUtf8Encoding.GetBytes(value, rentedBuffer.Bytes, 0, Utf8Encodings.Strict);
579+
580+
SetBytes(rentedBuffer.Bytes, actualLength);
576581
}
577582
else
578583
{
579-
bytes = Utf8Encodings.Strict.GetBytes(value);
580-
if (Array.IndexOf<byte>(bytes, 0) != -1)
584+
using var rentedSegmentEncoded = Utf8Encodings.Strict.GetBytesUsingThreadStaticBuffer(value);
585+
var segmentEncoded = rentedSegmentEncoded.Segment;
586+
actualLength = segmentEncoded.Count;
587+
588+
if (Array.IndexOf<byte>(segmentEncoded.Array, 0, 0, actualLength) != -1)
581589
{
582590
throw new ArgumentException("A CString cannot contain null bytes.", "value");
583591
}
584-
actualLength = bytes.Length;
592+
593+
SetBytes(segmentEncoded.Array, actualLength);
585594
}
586595

587-
_buffer.SetBytes(_position, bytes, 0, actualLength);
588-
_buffer.SetByte(_position + actualLength, 0);
596+
void SetBytes(byte[] bytes, int lenght)
597+
{
598+
_buffer.SetBytes(_position, bytes, 0, actualLength);
599+
_buffer.SetByte(_position + actualLength, 0);
600+
}
589601
}
590602

591603
SetPositionAfterWrite(_position + actualLength + 1);
@@ -715,17 +727,9 @@ public override void WriteString(string value, UTF8Encoding encoding)
715727
}
716728
else
717729
{
718-
byte[] bytes;
719-
if (maxLength <= _tempUtf8.Length)
720-
{
721-
bytes = _tempUtf8;
722-
actualLength = encoding.GetBytes(value, 0, value.Length, bytes, 0);
723-
}
724-
else
725-
{
726-
bytes = encoding.GetBytes(value);
727-
actualLength = bytes.Length;
728-
}
730+
using var rentedSegmentEncoded = encoding.GetBytesUsingThreadStaticBuffer(value);
731+
var bytes = rentedSegmentEncoded.Segment.Array;
732+
actualLength = rentedSegmentEncoded.Segment.Count;
729733

730734
var lengthPlusOneBytes = BitConverter.GetBytes(actualLength + 1);
731735

src/MongoDB.Bson/IO/EncodingHelper.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* Copyright 2020-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Text;
18+
19+
namespace MongoDB.Bson.IO
20+
{
21+
/// <summary>
22+
/// Represents a class that has some helper methods for <see cref="Encoding"/>.
23+
/// </summary>
24+
internal static class EncodingHelper
25+
{
26+
public readonly struct DisposableSegment : IDisposable
27+
{
28+
private IDisposable DisposableData { get; }
29+
public ArraySegment<byte> Segment { get; }
30+
31+
public DisposableSegment(IDisposable disposableData, ArraySegment<byte> segment)
32+
{
33+
DisposableData = disposableData;
34+
Segment = segment;
35+
}
36+
37+
public void Dispose()
38+
{
39+
DisposableData?.Dispose();
40+
}
41+
}
42+
43+
private static readonly ArraySegment<byte> __emptySegment = new ArraySegment<byte>(new byte[0]);
44+
45+
public static DisposableSegment GetBytesUsingThreadStaticBuffer(this Encoding encoding, string value)
46+
{
47+
if (encoding == null)
48+
{
49+
throw new ArgumentNullException(nameof(encoding));
50+
}
51+
52+
if (value == null)
53+
{
54+
throw new ArgumentNullException(nameof(value));
55+
}
56+
57+
var length = value.Length;
58+
if (length == 0)
59+
{
60+
return new DisposableSegment(null, __emptySegment);
61+
}
62+
63+
var maxSize = encoding.GetMaxByteCount(length);
64+
var rentedBuffer = ThreadStaticBuffer.RentBuffer(maxSize);
65+
66+
var size = encoding.GetBytes(value, 0, length, rentedBuffer.Bytes, 0);
67+
var segment = new ArraySegment<byte>(rentedBuffer.Bytes, 0, size);
68+
69+
return new DisposableSegment(rentedBuffer, segment);
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)