Skip to content

Commit 0f5c755

Browse files
authored
Merge pull request connamara#946 from gbirchmeier/rebase-VAllens-getbytes
[Rebase connamara#940] Refactor the GetBytes method of the CharEncoding class to use ArrayPool<byte> to improve memory allocation performance.
2 parents d942436 + 372373d commit 0f5c755

File tree

10 files changed

+177
-60
lines changed

10 files changed

+177
-60
lines changed

QuickFIXn/CharEncoding.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,64 @@
1-
namespace QuickFix;
1+
using System;
2+
using System.Buffers;
3+
4+
namespace QuickFix;
25

36
/// <summary>
47
/// Static class that manages the selected character encoding.
58
/// This impl is kind of a hack -- we're using a static class
69
/// so we don't have to alter a bunch of other classes to pass
710
/// a CharEncoding reference around.
811
/// </summary>
9-
public static class CharEncoding {
12+
public static class CharEncoding
13+
{
1014
public const string DefaultCharEncoding = "iso-8859-1";
1115

1216
public static System.Text.Encoding SelectedEncoding { get; private set; }
1317

14-
static CharEncoding() {
18+
static CharEncoding()
19+
{
1520
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
1621
SelectedEncoding = System.Text.Encoding.GetEncoding(DefaultCharEncoding);
1722
}
1823

19-
public static void ResetToDefaultEncoding() {
20-
SetEncoding(DefaultCharEncoding);
21-
}
24+
public static void ResetToDefaultEncoding() => SetEncoding(DefaultCharEncoding);
25+
26+
public static void SetEncoding(string encoding) => SelectedEncoding = System.Text.Encoding.GetEncoding(encoding);
27+
28+
/// <summary>
29+
/// Convert a string to a byte array using the current globally-set encoding.
30+
/// </summary>
31+
/// <param name="data"></param>
32+
/// <returns></returns>
33+
public static byte[] GetBytes(string data) => SelectedEncoding.GetBytes(data);
2234

23-
public static void SetEncoding(string encoding) {
24-
SelectedEncoding = System.Text.Encoding.GetEncoding(encoding);
35+
/// <summary>
36+
/// Convert a string to a byte array using the current globally-set encoding.
37+
/// This function uses a shared byte-pool for efficiency.
38+
/// The return value needs to be disposed; `using` is recommended (see existing usages).
39+
/// This function is still internal; we're not sure if we want to make it part of the public interface.
40+
/// </summary>
41+
/// <param name="data"></param>
42+
/// <param name="bytes"></param>
43+
/// <returns></returns>
44+
internal static ValueDisposable GetBytes(ReadOnlySpan<char> data, out ReadOnlySpan<byte> bytes)
45+
{
46+
System.Text.Encoding encoding = SelectedEncoding;
47+
int byteCount = encoding.GetByteCount(data);
48+
byte[] buffer = ArrayPool<byte>.Shared.Rent(byteCount);
49+
50+
encoding.GetBytes(data, new Span<byte>(buffer, 0, byteCount));
51+
bytes = new ReadOnlySpan<byte>(buffer, 0, byteCount);
52+
53+
return new ValueDisposable(buffer);
2554
}
55+
}
2656

27-
public static byte[] GetBytes(string data) {
28-
return SelectedEncoding.GetBytes(data);
57+
internal readonly ref struct ValueDisposable(byte[]? bytes)
58+
{
59+
public void Dispose()
60+
{
61+
if (bytes == null) return;
62+
if (bytes.Length > 0) ArrayPool<byte>.Shared.Return(bytes);
2963
}
3064
}

QuickFIXn/Fields/FieldBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,13 @@ public int GetTotal()
9999
MakeStringFields();
100100

101101
int sum = 0;
102-
byte[] array = CharEncoding.GetBytes(_stringField);
102+
103+
using ValueDisposable _ = CharEncoding.GetBytes(_stringField, out ReadOnlySpan<byte> array);
103104
foreach (byte b in array)
104105
{
105106
sum += b;
106107
}
108+
107109
return sum + 1; // +1 for SOH
108110
}
109111

QuickFIXn/Parser.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ public Parser(Encoding encoding)
3030
_checkSumBytes = encoding.GetBytes('\u0001' + "10=");
3131
}
3232

33-
public void AddToStream(byte[] data, int bytesAdded)
34-
=> AddToStream(data.AsSpan(0, bytesAdded));
33+
[Obsolete("Use AddToStream(ReadOnlySpan<byte>) instead. This will be removed in v1.15")]
34+
public void AddToStream(byte[] data, int bytesAdded) => AddToStream(new ReadOnlySpan<byte>(data, 0, bytesAdded));
3535

36-
public void AddToStream(Span<byte> data)
36+
public void AddToStream(ReadOnlySpan<byte> data)
3737
{
3838
// We attempt to copy the new bytes into the existing buffer.
3939
if (data.TryCopyTo(_buffer.AsSpan(_bufferStartIndex + _usedBufferLength)))

QuickFIXn/SocketInitiatorThread.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public bool Read()
9090
{
9191
int bytesRead = ReadSome(_readBuffer, 1000);
9292
if (bytesRead > 0)
93-
_parser.AddToStream(_readBuffer, bytesRead);
93+
_parser.AddToStream(new ReadOnlySpan<byte>(_readBuffer, 0, bytesRead));
9494
else
9595
Session.Next();
9696

@@ -189,8 +189,9 @@ public bool Send(string data)
189189
throw new ApplicationException("Initiator is not connected (uninitialized stream)");
190190
}
191191

192-
byte[] rawData = CharEncoding.GetBytes(data);
193-
_stream.Write(rawData, 0, rawData.Length);
192+
using ValueDisposable _ = CharEncoding.GetBytes(data, out ReadOnlySpan<byte> rawData);
193+
_stream.Write(rawData);
194+
194195
return true;
195196
}
196197

QuickFIXn/SocketReader.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void Read()
4646
{
4747
int bytesRead = ReadSome(_readBuffer, 1000);
4848
if (bytesRead > 0)
49-
_parser.AddToStream(_readBuffer, bytesRead);
49+
_parser.AddToStream(new ReadOnlySpan<byte>(_readBuffer, 0, bytesRead));
5050
else
5151
_qfSession?.Next();
5252

@@ -271,8 +271,9 @@ private void LogEvent(string s)
271271

272272
public int Send(string data)
273273
{
274-
byte[] rawData = CharEncoding.GetBytes(data);
275-
_stream.Write(rawData, 0, rawData.Length);
274+
using ValueDisposable _ = CharEncoding.GetBytes(data, out ReadOnlySpan<byte> rawData);
275+
_stream.Write(rawData);
276+
276277
return rawData.Length;
277278
}
278279

QuickFIXn/Store/FileStore.cs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Buffers;
23
using System.Collections.Generic;
34
using System.Text;
45
using QuickFix.Util;
@@ -10,16 +11,11 @@ namespace QuickFix.Store;
1011
/// </summary>
1112
public class FileStore : IMessageStore
1213
{
13-
private class MsgDef
14+
private readonly struct MsgDef(long index, int size)
1415
{
15-
public long Index { get; }
16-
public int Size { get; }
16+
public long Index { get; } = index;
1717

18-
public MsgDef(long index, int size)
19-
{
20-
Index = index;
21-
Size = size;
22-
}
18+
public int Size { get; } = size;
2319
}
2420

2521
private readonly string _seqNumsFileName;
@@ -198,13 +194,19 @@ public void Get(SeqNumType startSeqNum, SeqNumType endSeqNum, List<string> messa
198194
{
199195
for (SeqNumType i = startSeqNum; i <= endSeqNum; i++)
200196
{
201-
if (_offsets.ContainsKey(i))
197+
if (_offsets.TryGetValue(i, out MsgDef msgDef))
202198
{
203-
_msgFile.Seek(_offsets[i].Index, System.IO.SeekOrigin.Begin);
204-
byte[] msgBytes = new byte[_offsets[i].Size];
205-
_msgFile.Read(msgBytes, 0, msgBytes.Length);
206-
207-
messages.Add(CharEncoding.SelectedEncoding.GetString(msgBytes));
199+
_msgFile.Seek(msgDef.Index, System.IO.SeekOrigin.Begin);
200+
byte[] msgBytes = ArrayPool<byte>.Shared.Rent(msgDef.Size);
201+
try
202+
{
203+
_msgFile.ReadExactly(new Span<byte>(msgBytes, 0, msgDef.Size));
204+
messages.Add(CharEncoding.SelectedEncoding.GetString(new ReadOnlySpan<byte>(msgBytes, 0, msgDef.Size)));
205+
}
206+
finally
207+
{
208+
ArrayPool<byte>.Shared.Return(msgBytes);
209+
}
208210
}
209211
}
210212

@@ -221,20 +223,19 @@ public bool Set(SeqNumType msgSeqNum, string msg)
221223
_msgFile.Seek(0, System.IO.SeekOrigin.End);
222224

223225
long offset = _msgFile.Position;
224-
byte[] msgBytes = CharEncoding.GetBytes(msg);
225-
int size = msgBytes.Length;
226+
227+
using ValueDisposable _ = CharEncoding.GetBytes(msg.AsSpan(), out ReadOnlySpan<byte> msgBytes);
226228

227229
StringBuilder b = new StringBuilder();
228-
b.Append(msgSeqNum).Append(',').Append(offset).Append(',').Append(size);
230+
b.Append(msgSeqNum).Append(',').Append(offset).Append(',').Append(msgBytes.Length);
229231
_headerFile.WriteLine(b.ToString());
230232
_headerFile.Flush();
231233

232-
_offsets[msgSeqNum] = new MsgDef(offset, size);
234+
_offsets[msgSeqNum] = new MsgDef(offset, msgBytes.Length);
233235

234-
_msgFile.Write(msgBytes, 0, size);
236+
_msgFile.Write(msgBytes);
235237
_msgFile.Flush();
236238

237-
238239
return true;
239240
}
240241

@@ -299,7 +300,7 @@ public void Dispose()
299300
Dispose(true);
300301
GC.SuppressFinalize(this);
301302
}
302-
private bool _disposed = false;
303+
private bool _disposed;
303304
protected virtual void Dispose(bool disposing)
304305
{
305306
if (_disposed) return;

RELEASE_NOTES.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,38 @@ What's New
99
----------
1010

1111
**CAUTION:**
12-
* **1.13.0 has moved to .NET 8 (as Microsoft is ending .NET 6 support on Nov 12, 2024)
12+
* **Starting with 1.14, the QuickFIX message **nuget** packages have been renamed!**
13+
**Please remove the old package and import the new package!**
14+
(See issue #627 for more information.)
15+
16+
The new names are as follows (note the deleted period):
17+
* ~~QuickFIX.FIX4.0.{ver}~~ becomes **QuickFIX.FIX40.{ver}**
18+
* ~~QuickFIX.FIX4.1.{ver}~~ becomes **QuickFIX.FIX41.{ver}**
19+
* ~~QuickFIX.FIX4.2.{ver}~~ becomes **QuickFIX.FIX42.{ver}**
20+
* ~~QuickFIX.FIX4.3.{ver}~~ becomes **QuickFIX.FIX43.{ver}**
21+
* ~~QuickFIX.FIX4.4.{ver}~~ becomes **QuickFIX.FIX44.{ver}**
22+
* ~~QuickFIX.FIX5.0.{ver}~~ becomes **QuickFIX.FIX50.{ver}**
23+
* ~~QuickFIX.FIX5.0SP1.{ver}~~ becomes **QuickFIX.FIX50SP1.{ver}**
24+
* ~~QuickFIX.FIX5.0SP2.{ver}~~ becomes **QuickFIX.FIX50SP2.{ver}**
25+
* ~~QuickFIX.FIXT1.1.{ver}~~ becomes **QuickFIX.FIXT11.{ver}**
26+
27+
* **1.13.0 has moved to .NET 8 (as Microsoft is ending .NET 6 support on Nov 12, 2024)**
1328
* **There are breaking changes between 1.12 and 1.13! Please review the 1.13.0 notes below.**
1429
* **There are breaking changes between 1.11 and 1.12! Please review the 1.12.0 notes below.**
1530
* **There are breaking changes between 1.10 and 1.11! Please review the 1.11.0 notes below.**
1631

1732
### next release
1833

34+
**Breaking changes**
35+
* #627 - rename message packages to get rid of superfluous period (gbirchmeier)
36+
* e.g. QuickFIX.FIX4.4 is now QuickFIX.FIX44, etc.
37+
1938
**Non-breaking changes**
2039
* #939 - minor checkTooHigh/checkTooLow refactor in Session.cs (gbirchmeier)
2140
* #941 - clarify ResendRequest-related log message, add UT coverage for Session (gbirchmeier)
2241
* #895 - fix: When SSLCACertificate is empty an error is logged and it fails to start (dckorben)
2342
* #942 - fix #942: field 369 (LastMsgSeqNumProcessed) wrong in ResendRequest message (gbirchmeier)
43+
* #940 - Create an alternate CharEncoding.GetBytes impl which uses ArrayPool to improve memory performance (VAllens)
2444

2545
### v1.13.0
2646

UnitTests/CharEncodingTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Text;
3+
using NUnit.Framework;
4+
using QuickFix;
5+
6+
namespace UnitTests;
7+
8+
[TestFixture]
9+
public class CharEncodingTests
10+
{
11+
[TearDown]
12+
public void TearDown()
13+
{
14+
CharEncoding.ResetToDefaultEncoding();
15+
}
16+
17+
[Test]
18+
public void GetBytes_Simple()
19+
{
20+
CharEncoding.SetEncoding("iso-8859-1");
21+
byte[] isoBytes = CharEncoding.GetBytes("ïèâî");
22+
Assert.That(isoBytes, Is.EqualTo(new byte[] {0xEF, 0xE8, 0xE2, 0xEE}));
23+
24+
CharEncoding.SetEncoding("windows-1251");
25+
byte[] altBytes = CharEncoding.GetBytes("пиво");
26+
Assert.That(altBytes, Is.EqualTo(new byte[] {0xEF, 0xE8, 0xE2, 0xEE}));
27+
}
28+
29+
[Test]
30+
public void GetBytes_Pooled()
31+
{
32+
CharEncoding.SetEncoding("iso-8859-1");
33+
using ValueDisposable _ = CharEncoding.GetBytes("ïèâî", out ReadOnlySpan<byte> isoByteSpan);
34+
Assert.That(isoByteSpan.ToArray(), Is.EqualTo(new byte[] {0xEF, 0xE8, 0xE2, 0xEE}));
35+
36+
CharEncoding.SetEncoding("windows-1251");
37+
using ValueDisposable _2 = CharEncoding.GetBytes("пиво", out ReadOnlySpan<byte> altByteSpan);
38+
Assert.That(altByteSpan.ToArray(), Is.EqualTo(new byte[] {0xEF, 0xE8, 0xE2, 0xEE}));
39+
}
40+
}

0 commit comments

Comments
 (0)