diff --git a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs index cee5a3825ccf1..cff12010c283f 100644 --- a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs +++ b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs @@ -8,11 +8,13 @@ #endregion using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security; #if NET5_0_OR_GREATER using System.Runtime.CompilerServices; @@ -116,12 +118,24 @@ public void AddEntriesFrom(ref ParseContext ctx, FieldCodec codec) { EnsureSize(count + (length / codec.FixedSize)); - while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state)) + // if littleEndian treat array as bytes and directly copy from buffer for improved performance + if(TryGetArrayAsSpanPinnedUnsafe(codec, out Span span, out GCHandle handle)) + { + span = span.Slice(count * codec.FixedSize); + Debug.Assert(span.Length >= length); + ParsingPrimitives.ReadPackedFieldLittleEndian(ref ctx.buffer, ref ctx.state, length, span); + count += length / codec.FixedSize; + handle.Free(); + } + else { - // Only FieldCodecs with a fixed size can reach here, and they are all known - // types that don't allow the user to specify a custom reader action. - // reader action will never return null. - array[count++] = reader(ref ctx); + while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state)) + { + // Only FieldCodecs with a fixed size can reach here, and they are all known + // types that don't allow the user to specify a custom reader action. + // reader action will never return null. + array[count++] = reader(ref ctx); + } } } else @@ -241,9 +255,21 @@ public void WriteTo(ref WriteContext ctx, FieldCodec codec) int size = CalculatePackedDataSize(codec); ctx.WriteTag(tag); ctx.WriteLength(size); - for (int i = 0; i < count; i++) + + // if littleEndian and elements has fixed size, treat array as bytes (and write it as bytes to buffer) for improved performance + if(TryGetArrayAsSpanPinnedUnsafe(codec, out Span span, out GCHandle handle)) { - writer(ref ctx, array[i]); + span = span.Slice(0, Count * codec.FixedSize); + + WritingPrimitives.WriteRawBytes(ref ctx.buffer, ref ctx.state, span); + handle.Free(); + } + else + { + for (int i = 0; i < count; i++) + { + writer(ref ctx, array[i]); + } } } else @@ -679,6 +705,24 @@ internal void SetCount(int targetCount) count = targetCount; } + [SecuritySafeCritical] + private unsafe bool TryGetArrayAsSpanPinnedUnsafe(FieldCodec codec, out Span span, out GCHandle handle) + { + // 1. protobuf wire bytes is LittleEndian only + // 2. validate that size of csharp element T is matching the size of protobuf wire size + // NOTE: cannot use bool with this span because csharp marshal it as 4 bytes + if (BitConverter.IsLittleEndian && (codec.FixedSize > 0 && Marshal.SizeOf(typeof(T)) == codec.FixedSize)) + { + handle = GCHandle.Alloc(array, GCHandleType.Pinned); + span = new Span(handle.AddrOfPinnedObject().ToPointer(), array.Length * codec.FixedSize); + return true; + } + + span = default; + handle = default; + return false; + } + #region Explicit interface implementation for IList and ICollection. bool IList.IsFixedSize => false; diff --git a/csharp/src/Google.Protobuf/ParsingPrimitives.cs b/csharp/src/Google.Protobuf/ParsingPrimitives.cs index 1615ec832f3ca..a877f38f66447 100644 --- a/csharp/src/Google.Protobuf/ParsingPrimitives.cs +++ b/csharp/src/Google.Protobuf/ParsingPrimitives.cs @@ -11,6 +11,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -45,7 +46,7 @@ public static int ParseLength(ref ReadOnlySpan buffer, ref ParserInternalS /// /// Parses the next tag. - /// If the end of logical stream was reached, an invalid tag of 0 is returned. + /// If the end of logical stream was reached, an invalid tag of 0 is returned. /// public static uint ParseTag(ref ReadOnlySpan buffer, ref ParserInternalState state) { @@ -382,7 +383,7 @@ public static float ParseFloat(ref ReadOnlySpan buffer, ref ParserInternal // ReadUnaligned uses processor architecture for endianness. float result = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer.Slice(state.bufferPos, length))); state.bufferPos += length; - return result; + return result; } private static unsafe float ParseFloatSlow(ref ReadOnlySpan buffer, ref ParserInternalState state) @@ -737,7 +738,7 @@ public static uint ReadRawVarint32(Stream input) /// /// /// ZigZag encodes signed integers into values that can be efficiently - /// encoded with varint. (Otherwise, negative values must be + /// encoded with varint. (Otherwise, negative values must be /// sign-extended to 32 bits to be varint encoded, thus always taking /// 5 bytes on the wire.) /// @@ -751,7 +752,7 @@ public static int DecodeZigZag32(uint n) /// /// /// ZigZag encodes signed integers into values that can be efficiently - /// encoded with varint. (Otherwise, negative values must be + /// encoded with varint. (Otherwise, negative values must be /// sign-extended to 64 bits to be varint encoded, thus always taking /// 10 bytes on the wire.) /// @@ -810,5 +811,25 @@ private static void ReadRawBytesIntoSpan(ref ReadOnlySpan buffer, ref Pars state.bufferPos += unreadSpan.Length; } } + + /// + /// Read LittleEndian packed field from buffer of specified length into a span. + /// The amount of data available and the current limit should be checked before calling this method. + /// + internal static void ReadPackedFieldLittleEndian(ref ReadOnlySpan buffer, ref ParserInternalState state, int length, Span outBuffer) + { + Debug.Assert(BitConverter.IsLittleEndian); + + if (length <= state.bufferSize - state.bufferPos) + { + buffer.Slice(state.bufferPos, length).CopyTo(outBuffer); + state.bufferPos += length; + } + else + { + ReadRawBytesIntoSpan(ref buffer, ref state, length, outBuffer); + } + } + } }