Skip to content

Commit

Permalink
Optimize writing small strings
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Jan 26, 2021
1 parent f6da785 commit 6b2595e
Showing 1 changed file with 48 additions and 19 deletions.
67 changes: 48 additions & 19 deletions csharp/src/Google.Protobuf/WritingPrimitives.cs
Expand Up @@ -32,6 +32,7 @@

using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
Expand Down Expand Up @@ -163,10 +164,26 @@ public static void WriteBool(ref Span<byte> buffer, ref WriterInternalState stat
/// </summary>
public static void WriteString(ref Span<byte> buffer, ref WriterInternalState state, string value)
{
// Optimise the case where we have enough space to write
// the string directly to the buffer, which should be common.
#if !NETSTANDARD1_1
const int MaxBytesPerChar = 3;
const int MaxSmallStringLength = 128 / MaxBytesPerChar;

// The string is small enough that the length will always be a 1 byte varint.
// Also there is enough space to write length + bytes to buffer.
// Write string directly to the buffer, and then write length.
// This saves calling GetByteCount on the string. We get the string length from GetBytes.
if (value.Length <= MaxSmallStringLength && buffer.Length - state.position - 1 >= value.Length * MaxBytesPerChar)
{
buffer[state.position++] = (byte)WriteStringToBuffer(buffer, ref state, value);
return;
}
#endif

int length = Utf8Encoding.GetByteCount(value);
WriteLength(ref buffer, ref state, length);

// Optimise the case where we have enough space to write
// the string directly to the buffer, which should be common.
if (buffer.Length - state.position >= length)
{
if (length == value.Length) // Must be all ASCII...
Expand All @@ -179,23 +196,7 @@ public static void WriteString(ref Span<byte> buffer, ref WriterInternalState st
}
else
{
#if NETSTANDARD1_1
// slowpath when Encoding.GetBytes(Char*, Int32, Byte*, Int32) is not available
byte[] bytes = Utf8Encoding.GetBytes(value);
WriteRawBytes(ref buffer, ref state, bytes);
#else
ReadOnlySpan<char> source = value.AsSpan();
int bytesUsed;
unsafe
{
fixed (char* sourceChars = &MemoryMarshal.GetReference(source))
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(buffer.Slice(state.position)))
{
bytesUsed = Utf8Encoding.GetBytes(sourceChars, source.Length, destinationBytes, buffer.Length);
}
}
state.position += bytesUsed;
#endif
WriteStringToBuffer(buffer, ref state, value);
}
}
else
Expand All @@ -209,6 +210,34 @@ public static void WriteString(ref Span<byte> buffer, ref WriterInternalState st
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int WriteStringToBuffer(Span<byte> buffer, ref WriterInternalState state, string value)
{
#if NETSTANDARD1_1
// slowpath when Encoding.GetBytes(Char*, Int32, Byte*, Int32) is not available
byte[] bytes = Utf8Encoding.GetBytes(value);
WriteRawBytes(ref buffer, ref state, bytes);
return bytes.Length;
#else
ReadOnlySpan<char> source = value.AsSpan();
int bytesUsed;
unsafe
{
fixed (char* sourceChars = &MemoryMarshal.GetReference(source))
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(buffer))
{
bytesUsed = Utf8Encoding.GetBytes(
sourceChars,
source.Length,
destinationBytes + state.position,
buffer.Length - state.position);
}
}
state.position += bytesUsed;
return bytesUsed;
#endif
}

/// <summary>
/// Write a byte string, without a tag, to the stream.
/// The data is length-prefixed.
Expand Down

0 comments on commit 6b2595e

Please sign in to comment.