Skip to content

Commit

Permalink
Use one-shot UTF-8 encoding when possible.
Browse files Browse the repository at this point in the history
This avoids allocating a EncoderNLS object and making multiple passes over the source data when the entire string could be converted at once. Additionally, UTF8Encoding.GetBytes is more likely to be highly optimized in the framework.
  • Loading branch information
bgrainger committed Aug 13, 2023
1 parent 845461c commit 461def2
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 15 deletions.
57 changes: 45 additions & 12 deletions src/MySqlConnector/MySqlParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,15 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
}
else if (Value is string stringValue)
{
WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringValue.AsSpan());
WriteString(writer, noBackslashEscapes, stringValue.AsSpan());
}
else if (Value is ReadOnlyMemory<char> readOnlyMemoryChar)
{
WriteString(writer, noBackslashEscapes, writeDelimiters: true, readOnlyMemoryChar.Span);
WriteString(writer, noBackslashEscapes, readOnlyMemoryChar.Span);
}
else if (Value is Memory<char> memoryChar)
{
WriteString(writer, noBackslashEscapes, writeDelimiters: true, memoryChar.Span);
WriteString(writer, noBackslashEscapes, memoryChar.Span);
}
else if (Value is char charValue)
{
Expand Down Expand Up @@ -447,12 +447,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
#if NETCOREAPP3_1_OR_GREATER
writer.Write((byte) '\'');
foreach (var chunk in stringBuilder.GetChunks())
WriteString(writer, noBackslashEscapes, writeDelimiters: false, chunk.Span);
WriteStringChunk(writer, noBackslashEscapes, chunk.Span);
if (stringBuilder.Length != 0)
writer.Write("".AsSpan(), flush: true);
writer.Write((byte) '\'');
#else
WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringBuilder.ToString().AsSpan());
WriteString(writer, noBackslashEscapes, stringBuilder.ToString().AsSpan());
#endif
}
else if (MySqlDbType == MySqlDbType.Int16)
Expand Down Expand Up @@ -494,10 +494,9 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
throw new NotSupportedException($"Parameter type {Value.GetType().Name} is not supported; see https://fl.vu/mysql-param-type. Value: {Value}");
}

static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool writeDelimiters, ReadOnlySpan<char> value)
static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan<char> value)
{
if (writeDelimiters)
writer.Write((byte) '\'');
writer.Write((byte) '\'');

var charsWritten = 0;
while (charsWritten < value.Length)
Expand All @@ -507,13 +506,13 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
if (nextDelimiterIndex == -1)
{
// write the rest of the string
writer.Write(remainingValue, flush: writeDelimiters);
writer.Write(remainingValue);
charsWritten += remainingValue.Length;
}
else
{
// write up to (and including) the delimiter, then double it
writer.Write(remainingValue[..nextDelimiterIndex], flush: true);
writer.Write(remainingValue[..nextDelimiterIndex]);
if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
writer.Write((ushort) 0x5C5C); // \\
else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes)
Expand All @@ -528,9 +527,43 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
}
}

if (writeDelimiters)
writer.Write((byte) '\'');
writer.Write((byte) '\'');
}

#if NETCOREAPP3_1_OR_GREATER
// Writes a partial chunk of a string (that may end with half of a surrogate pair), escaping any delimiter characters.
static void WriteStringChunk(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan<char> value)
{
var charsWritten = 0;
while (charsWritten < value.Length)
{
var remainingValue = value[charsWritten..];
var nextDelimiterIndex = remainingValue.IndexOfAny('\0', '\'', '\\');
if (nextDelimiterIndex == -1)
{
// write the rest of the string
writer.Write(remainingValue, flush: false);
charsWritten += remainingValue.Length;
}
else
{
// write up to (and including) the delimiter, then double it
writer.Write(remainingValue[..nextDelimiterIndex], flush: true);
if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
writer.Write((ushort) 0x5C5C); // \\
else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes)
writer.Write((byte) 0x5C); // \
else if (remainingValue[nextDelimiterIndex] == '\'')
writer.Write((ushort) 0x2727); // ''
else if (remainingValue[nextDelimiterIndex] == '\0' && !noBackslashEscapes)
writer.Write((ushort) 0x305C); // \0
else if (remainingValue[nextDelimiterIndex] == '\0' && noBackslashEscapes)
writer.Write((byte) 0x00); // (nul)
charsWritten += nextDelimiterIndex + 1;
}
}
}
#endif
}

internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions options)
Expand Down
17 changes: 14 additions & 3 deletions src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,22 @@ public void Write(ReadOnlySpan<byte> span)
m_output = m_output[span.Length..];
}

public void Write(string value) => Write(value.AsSpan(), flush: true);
public void Write(string value) => Write(value.AsSpan());

public void WriteAscii(string value) => WriteAscii(value.AsSpan());

public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length), flush: true);
public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length));

public void Write(ReadOnlySpan<char> chars)
{
if (m_output.Length < chars.Length * 3)
{
var neededBytes = Encoding.UTF8.GetByteCount(chars);
if (m_output.Length < neededBytes)
Reallocate(neededBytes);
}
m_output = m_output[Encoding.UTF8.GetBytes(chars, m_output.Span)..];
}

public void Write(ReadOnlySpan<char> chars, bool flush)
{
Expand Down Expand Up @@ -273,7 +284,7 @@ public static void WriteLengthEncodedString(this ByteBufferWriter writer, ReadOn
{
var byteCount = Encoding.UTF8.GetByteCount(value);
writer.WriteLengthEncodedInteger((ulong) byteCount);
writer.Write(value, flush: true);
writer.Write(value);
}

public static void WriteLengthEncodedAsciiString(this ByteBufferWriter writer, string value)
Expand Down

0 comments on commit 461def2

Please sign in to comment.