Skip to content

Commit

Permalink
Utilize stackalloc and access static byte data directly (#767)
Browse files Browse the repository at this point in the history
* Eliminate array allocation in MimeUtils.Unquote()
* Use stackalloc in Header.Unfold() to reduce string allocations
* Use stackalloc in Header constructor
* Eliminate two buffer allocations in DecodeRfc2231
* Use static ReadOnlySpan<byte> compiler feature to reference to static data directly in the encoders

See dotnet/roslyn#24621 & https://vcsjones.dev/csharp-readonly-span-bytes-static/ for
more information about the ReadOnlySpan<byte> static array usage.
  • Loading branch information
iamcarbon committed Feb 14, 2022
1 parent a19fa26 commit 045f5c0
Show file tree
Hide file tree
Showing 11 changed files with 42 additions and 18 deletions.
2 changes: 1 addition & 1 deletion MimeKit/Encodings/Base64Decoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace MimeKit.Encodings {
/// </remarks>
public class Base64Decoder : IMimeDecoder
{
static readonly byte[] base64_rank = new byte[256] {
static ReadOnlySpan<byte> base64_rank => new byte[256] {
255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63,
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/Encodings/Base64Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace MimeKit.Encodings {
/// </remarks>
public class Base64Encoder : IMimeEncoder
{
static readonly byte[] base64_alphabet = {
static ReadOnlySpan<byte> base64_alphabet => new byte[] {
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/Encodings/HexEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ namespace MimeKit.Encodings {
/// </remarks>
public class HexEncoder : IMimeEncoder
{
static readonly byte[] hex_alphabet = new byte[16] {
static ReadOnlySpan<byte> hex_alphabet => new byte[16] {
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7'
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F'
};
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/Encodings/QEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public enum QEncodeMode : byte {
/// </remarks>
public class QEncoder : IMimeEncoder
{
static readonly byte[] hex_alphabet = new byte[16] {
static ReadOnlySpan<byte> hex_alphabet => new byte[16] {
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7'
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F'
};
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/Encodings/QuotedPrintableEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace MimeKit.Encodings {
/// </remarks>
public class QuotedPrintableEncoder : IMimeEncoder
{
static readonly byte[] hex_alphabet = new byte[16] {
static ReadOnlySpan<byte> hex_alphabet => new byte[16] {
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7'
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F'
};
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/Encodings/UUDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace MimeKit.Encodings {
/// </remarks>
public class UUDecoder : IMimeDecoder
{
static readonly byte[] uudecode_rank = new byte[256] {
static ReadOnlySpan<byte> uudecode_rank => new byte[256] {
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
Expand Down
28 changes: 22 additions & 6 deletions MimeKit/Header.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,14 @@ protected Header (ParserOptions options, HeaderId id, string name, byte[] field,
/// <param name="fieldNameLength">The length of the field name (not including trailing whitespace).</param>
/// <param name="value">The raw value of the header.</param>
/// <param name="invalid"><c>true</c> if the header field is invalid; othereise, <c>false</c>.</param>
#if NET5_0_OR_GREATER
[System.Runtime.CompilerServices.SkipLocalsInit]
#endif
internal protected Header (ParserOptions options, byte[] field, int fieldNameLength, byte[] value, bool invalid)
{
var chars = new char[fieldNameLength];
Span<char> chars = fieldNameLength <= 32
? stackalloc char[32]
: new char[fieldNameLength];

for (int i = 0; i < fieldNameLength; i++)
chars[i] = (char) field[i];
Expand All @@ -323,7 +328,7 @@ internal protected Header (ParserOptions options, byte[] field, int fieldNameLen
rawField = field;
rawValue = value;

Field = new string (chars, 0, fieldNameLength);
Field = chars.Slice (0, fieldNameLength).ToString ();
Id = Field.ToHeaderId ();
IsInvalid = invalid;
}
Expand All @@ -340,9 +345,15 @@ internal protected Header (ParserOptions options, byte[] field, int fieldNameLen
/// <param name="field">The raw header field.</param>
/// <param name="value">The raw value of the header.</param>
/// <param name="invalid"><c>true</c> if the header field is invalid; othereise, <c>false</c>.</param>
#if NET5_0_OR_GREATER
[System.Runtime.CompilerServices.SkipLocalsInit]
#endif
internal protected Header (ParserOptions options, byte[] field, byte[] value, bool invalid)
{
var chars = new char[field.Length];
Span<char> chars = field.Length <= 32
? stackalloc char[32]
: new char[field.Length];

int count = 0;

while (count < field.Length && (invalid || !field[count].IsBlank ())) {
Expand All @@ -354,7 +365,7 @@ internal protected Header (ParserOptions options, byte[] field, byte[] value, bo
rawField = field;
rawValue = value;

Field = new string (chars, 0, count);
Field = chars.Slice (0, count).ToString ();
Id = Field.ToHeaderId ();
IsInvalid = invalid;
}
Expand Down Expand Up @@ -1306,6 +1317,9 @@ public override string ToString ()
/// </remarks>
/// <returns>The unfolded header value.</returns>
/// <param name="text">The header text.</param>
#if NET5_0_OR_GREATER
[System.Runtime.CompilerServices.SkipLocalsInit]
#endif
public static string Unfold (string text)
{
int startIndex;
Expand All @@ -1330,14 +1344,16 @@ public static string Unfold (string text)
}

int count = endIndex - startIndex;
char[] chars = new char[count];
Span<char> chars = count <= 32
? stackalloc char[32]
: new char[count];

for (i = startIndex, count = 0; i < endIndex; i++) {
if (text[i] != '\r' && text[i] != '\n')
chars[count++] = text[i];
}

return new string (chars, 0, count);
return chars.Slice (0, count).ToString ();
}

static bool IsAsciiAtom (byte c)
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/MimeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ enum MimeParserState : sbyte
/// </remarks>
public partial class MimeParser : IEnumerable<MimeMessage>
{
static readonly byte[] UTF8ByteOrderMark = { 0xEF, 0xBB, 0xBF };
static ReadOnlySpan<byte> UTF8ByteOrderMark => new byte[] { 0xEF, 0xBB, 0xBF };
const int ReadAheadSize = 128;
const int BlockSize = 4096;
const int PadSize = 4;
Expand Down
3 changes: 2 additions & 1 deletion MimeKit/MimeReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ enum MimeEntityType
Multipart
}

static readonly byte[] UTF8ByteOrderMark = { 0xEF, 0xBB, 0xBF };
static ReadOnlySpan<byte> UTF8ByteOrderMark => new byte[] { 0xEF, 0xBB, 0xBF };

static readonly Task CompletedTask;
const int HeaderBufferGrowSize = 64;
const int ReadAheadSize = 128;
Expand Down
12 changes: 9 additions & 3 deletions MimeKit/ParameterList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

using MimeKit.Encodings;
using MimeKit.Utils;
using System.Buffers;

namespace MimeKit {
/// <summary>
Expand Down Expand Up @@ -906,17 +907,22 @@ static string DecodeRfc2231 (out Encoding encoding, ref Decoder decoder, HexDeco
}

int length = endIndex - index;
var decoded = new byte[hex.EstimateOutputLength (length)];
var decoded = ArrayPool<byte>.Shared.Rent(hex.EstimateOutputLength (length));

// hex decode...
length = hex.Decode (text, index, length, decoded);

int outLength = decoder.GetCharCount (decoded, 0, length, flush);
var output = new char[outLength];
var output = ArrayPool<char>.Shared.Rent (outLength);

outLength = decoder.GetChars (decoded, 0, length, output, 0, flush);

return new string (output, 0, outLength);
var result = new string (output, 0, outLength);

ArrayPool<char>.Shared.Return (output);
ArrayPool<byte>.Shared.Return (decoded);

return result;
}

internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out ParameterList paramList)
Expand Down
3 changes: 2 additions & 1 deletion MimeKit/Utils/MimeUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static class MimeUtils
{
const string base36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
static string DefaultHostName = null;
static readonly char[] UnquoteChars = new[] { '\r', '\n', '\t', '\\', '"' };

/// <summary>
/// A string comparer that performs a case-insensitive ordinal string comparison.
Expand Down Expand Up @@ -524,7 +525,7 @@ public static string Unquote (string text)
if (text == null)
throw new ArgumentNullException (nameof (text));

int index = text.IndexOfAny (new [] { '\r', '\n', '\t', '\\', '"' });
int index = text.IndexOfAny (UnquoteChars);

if (index == -1)
return text;
Expand Down

0 comments on commit 045f5c0

Please sign in to comment.