diff --git a/csharp/src/Google.Protobuf/WritingPrimitives.cs b/csharp/src/Google.Protobuf/WritingPrimitives.cs index 846df7350260..030f06c7e1a9 100644 --- a/csharp/src/Google.Protobuf/WritingPrimitives.cs +++ b/csharp/src/Google.Protobuf/WritingPrimitives.cs @@ -32,6 +32,7 @@ using System; using System.Buffers.Binary; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; @@ -163,10 +164,26 @@ public static void WriteBool(ref Span buffer, ref WriterInternalState stat /// public static void WriteString(ref Span 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... @@ -179,23 +196,7 @@ public static void WriteString(ref Span 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 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 @@ -209,6 +210,34 @@ public static void WriteString(ref Span buffer, ref WriterInternalState st } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int WriteStringToBuffer(Span 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 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 + } + /// /// Write a byte string, without a tag, to the stream. /// The data is length-prefixed.