From 58e7f22441832b5f77c9c71071cd1cc3649b7945 Mon Sep 17 00:00:00 2001 From: Dameng <313880747@qq.com> Date: Thu, 2 Jul 2026 19:13:25 +0800 Subject: [PATCH 1/4] Optimize packed fixed-size repeated I/O Add fast little-endian memcopy paths for packed fixed-size repeated fields. Introduces src/LightProto/Serializer.PackedRepeated.cs with TryWritePackedRepeatedFieldLittleEndian, ParseRepeatedFieldIntoSpan and TryReadPackedRepeatedFieldLittleEndian (with NET5/NET8 optimizations). Update generator (Helper.cs) and parser files (Array.cs, IEnumerableReader.cs, IEnumerableWriter.cs) to use the fast paths; add ParsingPrimitives.ReadPackedFieldLittleEndian and update PublicAPI.Unshipped. Add tests for packed fixed32 serialization and ReadOnlySequence splitting in FixedSizePackedArrayTests and FixedSizePackedListTests. Improves performance and correctness when reading/writing packed primitive arrays/lists. --- src/LightProto.Generator/Helper.cs | 23 +- src/LightProto/Parser/Array.cs | 23 +- src/LightProto/Parser/IEnumerableReader.cs | 31 +-- src/LightProto/Parser/IEnumerableWriter.cs | 20 +- src/LightProto/ParsingPrimitives.cs | 17 ++ .../PublicAPI/net/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + src/LightProto/Serializer.PackedRepeated.cs | 229 ++++++++++++++++++ .../Parsers/FixedSizePackedArrayTests.cs | 19 ++ .../Parsers/FixedSizePackedListTests.cs | 19 ++ 10 files changed, 317 insertions(+), 68 deletions(-) create mode 100644 src/LightProto/Serializer.PackedRepeated.cs diff --git a/src/LightProto.Generator/Helper.cs b/src/LightProto.Generator/Helper.cs index a929b88..cb68e6d 100644 --- a/src/LightProto.Generator/Helper.cs +++ b/src/LightProto.Generator/Helper.cs @@ -680,6 +680,13 @@ private static void GenerateInlineArrayProtoWriter(CodeWriter writer, InlineArra writer.WriteLine("long size = CalculatePackedDataSize(collection);"); writer.WriteLine("output.WriteTag(Tag);"); writer.WriteLine("output.WriteLongLength(size);"); + writer.WriteLine( + "if (global::LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref output, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize))" + ); + using (writer.IndentScope()) + { + writer.WriteLine("return;"); + } writer.WriteLine("for (var index = 0; index < Length; index++)"); using (writer.IndentScope()) { @@ -726,27 +733,21 @@ private static void GenerateInlineArrayProtoReader(CodeWriter writer, InlineArra writer.WriteLine("object global::LightProto.Parser.ICollectionReader.Empty => Empty;"); writer.WriteLine($"public IProtoReader<{elementType}> ItemReader {{ get; }}"); writer.WriteLine($"public {inlineArrayType} Empty => new {inlineArrayType}();"); - writer.WriteLine($"private global::LightProto.Parser.ArrayProtoReader<{elementType}> ArrayReader {{ get; }}"); + writer.WriteLine("private int ItemFixedSize { get; }"); writer.WriteLine($"public {parserTypeName}(IProtoReader<{elementType}> itemReader, uint tag, int itemFixedSize)"); using (writer.IndentScope()) { writer.WriteLine("ItemReader = itemReader;"); - writer.WriteLine( - "ArrayReader = new global::LightProto.Parser.ArrayProtoReader<" + elementType + ">(itemReader, tag, itemFixedSize);" - ); + writer.WriteLine("ItemFixedSize = itemFixedSize;"); } writer.WriteLine($"public {inlineArrayType} ParseFrom(ref ReaderContext input)"); using (writer.IndentScope()) { - writer.WriteLine("var items = ArrayReader.ParseFrom(ref input);"); writer.WriteLine($"var collection = default({inlineArrayType});"); - writer.WriteLine("var count = Math.Min(items.Length, Length);"); - writer.WriteLine("for (var index = 0; index < count; index++)"); - using (writer.IndentScope()) - { - writer.WriteLine("collection[index] = items[index];"); - } + writer.WriteLine( + "global::LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref input, ItemReader, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize);" + ); writer.WriteLine("return collection;"); } } diff --git a/src/LightProto/Parser/Array.cs b/src/LightProto/Parser/Array.cs index 24bb0bd..aa4fddf 100644 --- a/src/LightProto/Parser/Array.cs +++ b/src/LightProto/Parser/Array.cs @@ -56,28 +56,7 @@ public TItem[] ParseFrom(ref ReaderContext ctx) { var count = length / fixedSize; var collection = new TItem[count]; - // if littleEndian treat array as bytes and directly copy from buffer for improved performance - // if ( - // collection is List list - // && BitConverter.IsLittleEndian - // && Marshal.SizeOf() == fixedSize - // ) - // { - // var itemSpan = CollectionsMarshal.AsSpan(list); - // - // var byteSpan = MemoryMarshal.CreateSpan( - // ref Unsafe.As(ref MemoryMarshal.GetReference(itemSpan)), - // checked(itemSpan.Length * fixedSize) - // ); - // ParsingPrimitives.ReadPackedFieldLittleEndian( - // ref ctx.buffer, - // ref ctx.state, - // length, - // byteSpan - // ); - // CollectionsMarshal.SetCount(list, count); - // } - // else + if (!Serializer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, collection.AsSpan(), fixedSize)) { int i = 0; while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state)) diff --git a/src/LightProto/Parser/IEnumerableReader.cs b/src/LightProto/Parser/IEnumerableReader.cs index 71f82d7..aada691 100644 --- a/src/LightProto/Parser/IEnumerableReader.cs +++ b/src/LightProto/Parser/IEnumerableReader.cs @@ -76,28 +76,15 @@ public TCollection ParseFrom(ref ReaderContext ctx) { var count = length / fixedSize; var collection = CreateWithCapacity((int)count); - // if littleEndian treat array as bytes and directly copy from buffer for improved performance - // if ( - // collection is List list - // && BitConverter.IsLittleEndian - // && Marshal.SizeOf() == fixedSize - // ) - // { - // var itemSpan = CollectionsMarshal.AsSpan(list); - // - // var byteSpan = MemoryMarshal.CreateSpan( - // ref Unsafe.As(ref MemoryMarshal.GetReference(itemSpan)), - // checked(itemSpan.Length * fixedSize) - // ); - // ParsingPrimitives.ReadPackedFieldLittleEndian( - // ref ctx.buffer, - // ref ctx.state, - // length, - // byteSpan - // ); - // CollectionsMarshal.SetCount(list, count); - // } - // else +#if NET8_0_OR_GREATER + if ( + collection is List list + && Serializer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, list, (int)count, fixedSize) + ) + { + return collection; + } +#endif { while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state)) { diff --git a/src/LightProto/Parser/IEnumerableWriter.cs b/src/LightProto/Parser/IEnumerableWriter.cs index 4804884..f63d421 100644 --- a/src/LightProto/Parser/IEnumerableWriter.cs +++ b/src/LightProto/Parser/IEnumerableWriter.cs @@ -124,20 +124,14 @@ public void WriteTo(ref WriterContext output, TCollection collection) output.WriteTag(Tag); output.WriteLongLength(size); - // 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)) - // { - // span = span.Slice(0, Count * codec.FixedSize); - // - // WritingPrimitives.WriteRawBytes(ref ctx.buffer, ref ctx.state, span); - // handle.Free(); - // } - // else + if (Serializer.TryWritePackedRepeatedFieldLittleEndian(ref output, collection, count, ItemFixedSize)) { - foreach (var item in collection) - { - ItemWriter.WriteMessageTo(ref output, item); - } + return; + } + + foreach (var item in collection) + { + ItemWriter.WriteMessageTo(ref output, item); } } else diff --git a/src/LightProto/ParsingPrimitives.cs b/src/LightProto/ParsingPrimitives.cs index 1a2abca..81ccf99 100644 --- a/src/LightProto/ParsingPrimitives.cs +++ b/src/LightProto/ParsingPrimitives.cs @@ -758,6 +758,23 @@ Span byteSpan } } + internal static void ReadPackedFieldLittleEndian( + ref ReadOnlySpan buffer, + ref ParserInternalState state, + int length, + Span destination + ) + { + if (length <= state.bufferSize - state.bufferPos) + { + buffer.Slice(state.bufferPos, length).CopyTo(destination); + state.bufferPos += length; + return; + } + + ReadRawBytesIntoSpan(ref buffer, ref state, length, destination); + } + public static void SkipLastField(ref ReadOnlySpan buffer, ref ParserInternalState state) { if (state.lastTag == 0) diff --git a/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt b/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt index b202d35..977a239 100644 --- a/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt @@ -15,6 +15,8 @@ static LightProto.CodedOutputStream.ComputeLongLengthSize(long length) -> int static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, object! value) -> long static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, T value) -> long static LightProto.Serializer.CalculateLongSize(T message) -> long +static LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int +static LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool static LightProto.InvalidProtocolBufferException.MissingRequiredMember(string! memberName) -> LightProto.InvalidProtocolBufferException! LightProto.Parser.ImmutableQueueProtoReader LightProto.Parser.ImmutableQueueProtoReader.Empty.get -> System.Collections.Immutable.ImmutableQueue! diff --git a/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 446ddad..c8ed12e 100644 --- a/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -14,6 +14,8 @@ LightProto.WriterContext.WriteLongLength(long length) -> void static LightProto.CodedOutputStream.ComputeLongLengthSize(long length) -> int static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, object! value) -> long static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, T value) -> long +static LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int +static LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool static LightProto.InvalidProtocolBufferException.MissingRequiredMember(string! memberName) -> LightProto.InvalidProtocolBufferException! LightProto.Parser.ImmutableQueueProtoReader LightProto.Parser.ImmutableQueueProtoReader.Empty.get -> System.Collections.Immutable.ImmutableQueue! diff --git a/src/LightProto/Serializer.PackedRepeated.cs b/src/LightProto/Serializer.PackedRepeated.cs new file mode 100644 index 0000000..cbf8769 --- /dev/null +++ b/src/LightProto/Serializer.PackedRepeated.cs @@ -0,0 +1,229 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LightProto +{ + public static partial class Serializer + { + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool TryWritePackedRepeatedFieldLittleEndian(ref WriterContext output, ReadOnlySpan values, int itemFixedSize) + { + if (!TryGetBytes(values, itemFixedSize, out var bytes)) + { + return false; + } + + WritingPrimitives.WriteRawBytes(ref output.buffer, ref output.state, bytes); + return true; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static int ParseRepeatedFieldIntoSpan( + ref ReaderContext input, + IProtoReader itemReader, + Span destination, + int itemFixedSize + ) + { + var tag = input.state.lastTag; + var writtenCount = 0; + + if (WireFormat.GetTagWireType(tag) is WireFormat.WireType.LengthDelimited && PackedRepeated.Support()) + { + var length = input.ReadLength(); + if (length <= 0) + { + return 0; + } + + var oldLimit = SegmentedBufferHelper.PushLimit(ref input.state, length); + try + { + if (itemFixedSize > 0 && length % itemFixedSize == 0 && ParsingPrimitives.IsDataAvailable(ref input.state, length)) + { + var itemCount = length / itemFixedSize; + if ( + itemCount <= destination.Length + && TryReadPackedRepeatedFieldLittleEndian(ref input, length, destination.Slice(0, itemCount), itemFixedSize) + ) + { + return itemCount; + } + } + + while (!SegmentedBufferHelper.IsReachedLimit(ref input.state)) + { + var item = ParseMessageFrom(itemReader, ref input); + if (writtenCount < destination.Length) + { + destination[writtenCount] = item; + } + + writtenCount++; + } + + return Math.Min(writtenCount, destination.Length); + } + finally + { + SegmentedBufferHelper.PopLimit(ref input.state, oldLimit); + } + } + + do + { + var item = ParseMessageFrom(itemReader, ref input); + if (writtenCount < destination.Length) + { + destination[writtenCount] = item; + } + + writtenCount++; + } while (ParsingPrimitives.MaybeConsumeTag(ref input.buffer, ref input.state, tag)); + + return Math.Min(writtenCount, destination.Length); + } + + internal static bool TryWritePackedRepeatedFieldLittleEndian( + ref WriterContext output, + TCollection collection, + int count, + int itemFixedSize + ) + where TCollection : IEnumerable + { + if (collection is TItem[] array) + { + return TryWritePackedRepeatedFieldLittleEndian(ref output, array.AsSpan(0, count), itemFixedSize); + } + +#if NET5_0_OR_GREATER + if (collection is List list) + { + return TryWritePackedRepeatedFieldLittleEndian(ref output, CollectionsMarshal.AsSpan(list).Slice(0, count), itemFixedSize); + } +#endif + + return false; + } + +#if NET8_0_OR_GREATER + internal static bool TryReadPackedRepeatedFieldLittleEndian( + ref ReaderContext input, + long byteLength, + List destination, + int count, + int itemFixedSize + ) + { + if (!CanUseLittleEndianPackedMemoryCopy(itemFixedSize)) + { + return false; + } + + CollectionsMarshal.SetCount(destination, count); + return TryReadPackedRepeatedFieldLittleEndian( + ref input, + byteLength, + CollectionsMarshal.AsSpan(destination).Slice(0, count), + itemFixedSize + ); + } +#endif + + internal static bool TryReadPackedRepeatedFieldLittleEndian( + ref ReaderContext input, + long byteLength, + Span destination, + int itemFixedSize + ) + { + if (byteLength > int.MaxValue || !TryGetBytes(destination, itemFixedSize, out var bytes)) + { + return false; + } + + var length = (int)byteLength; + if (bytes.Length < length) + { + return false; + } + + ParsingPrimitives.ReadPackedFieldLittleEndian(ref input.buffer, ref input.state, length, bytes.Slice(0, length)); + return true; + } + + private static bool TryGetBytes(ReadOnlySpan values, int itemFixedSize, out ReadOnlySpan bytes) + { + bytes = default; + if (!CanUseLittleEndianPackedMemoryCopy(itemFixedSize)) + { + return false; + } + + if (values.IsEmpty) + { + bytes = ReadOnlySpan.Empty; + return true; + } + + bytes = MemoryMarshal.CreateSpan( + ref Unsafe.As(ref MemoryMarshal.GetReference(values)), + checked(values.Length * itemFixedSize) + ); + return true; + } + + private static bool TryGetBytes(Span values, int itemFixedSize, out Span bytes) + { + bytes = default; + if (!CanUseLittleEndianPackedMemoryCopy(itemFixedSize)) + { + return false; + } + + if (values.IsEmpty) + { + bytes = Span.Empty; + return true; + } + + bytes = MemoryMarshal.CreateSpan( + ref Unsafe.As(ref MemoryMarshal.GetReference(values)), + checked(values.Length * itemFixedSize) + ); + return true; + } + + private static bool CanUseLittleEndianPackedMemoryCopy(int itemFixedSize) + { + if (!BitConverter.IsLittleEndian || itemFixedSize <= 0) + { + return false; + } + + var type = typeof(T); + if ( + type != typeof(int) + && type != typeof(uint) + && type != typeof(long) + && type != typeof(ulong) + && type != typeof(float) + && type != typeof(double) + ) + { + return false; + } + + try + { + return Marshal.SizeOf() == itemFixedSize; + } + catch (ArgumentException) + { + return false; + } + } + } +} diff --git a/tests/LightProto.Tests/Parsers/FixedSizePackedArrayTests.cs b/tests/LightProto.Tests/Parsers/FixedSizePackedArrayTests.cs index dc698c1..21916a7 100644 --- a/tests/LightProto.Tests/Parsers/FixedSizePackedArrayTests.cs +++ b/tests/LightProto.Tests/Parsers/FixedSizePackedArrayTests.cs @@ -28,6 +28,25 @@ public override IEnumerable GetMessages() yield return new() { Property = [] }; } + [Test] + public async Task LightProto_Serialize_WritesPackedFixed32Bytes() + { + var bytes = new Message { Property = [1, -2] }.ToByteArray(Message.ProtoWriter); + + await Assert.That(bytes).IsEquivalentTo(new byte[] { 10, 8, 1, 0, 0, 0, 254, 255, 255, 255 }); + } + + [Test] + public async Task LightProto_Deserialize_ReadOnlySequenceSplitInsideFixed32Values() + { + var bytes = new Message { Property = [1, -2, 3] }.ToByteArray(Message.ProtoWriter); + var sequence = LightProto.Tests.SerializerTests.GetReadonlySequence(bytes.Chunk(1).ToArray()); + + var parsed = Serializer.Deserialize(sequence, Message.ProtoReader); + + await Assert.That(parsed.Property).IsEquivalentTo(new[] { 1, -2, 3 }); + } + public override IEnumerable GetGoogleMessages() { return GetMessages().Select(o => new FixedSizeArrayTestsMessage() { Property = { o.Property } }); diff --git a/tests/LightProto.Tests/Parsers/FixedSizePackedListTests.cs b/tests/LightProto.Tests/Parsers/FixedSizePackedListTests.cs index d93b5f4..183652b 100644 --- a/tests/LightProto.Tests/Parsers/FixedSizePackedListTests.cs +++ b/tests/LightProto.Tests/Parsers/FixedSizePackedListTests.cs @@ -37,6 +37,25 @@ public override IEnumerable GetMessages() yield return new() { Property = [] }; } + [Test] + public async Task LightProto_Serialize_WritesPackedFixed32Bytes() + { + var bytes = new Message { Property = [1, -2] }.ToByteArray(Message.ProtoWriter); + + await Assert.That(bytes).IsEquivalentTo(new byte[] { 10, 8, 1, 0, 0, 0, 254, 255, 255, 255 }); + } + + [Test] + public async Task LightProto_Deserialize_ReadOnlySequenceSplitInsideFixed32Values() + { + var bytes = new Message { Property = [1, -2, 3] }.ToByteArray(Message.ProtoWriter); + var sequence = LightProto.Tests.SerializerTests.GetReadonlySequence(bytes.Chunk(1).ToArray()); + + var parsed = Serializer.Deserialize(sequence, Message.ProtoReader); + + await Assert.That(parsed.Property).IsEquivalentTo(new[] { 1, -2, 3 }); + } + public override IEnumerable GetGoogleMessages() { return GetMessages().Select(o => new FixedSizeArrayTestsMessage() { Property = { o.Property } }); From 498906893fb73e9a7fe3afae020748f360252058 Mon Sep 17 00:00:00 2001 From: Dameng <313880747@qq.com> Date: Thu, 2 Jul 2026 19:45:17 +0800 Subject: [PATCH 2/4] Extract PackedRepeated helpers to PackedRepeatedOptimizer Move packed-repeated helper methods out of Serializer into a new PackedRepeatedOptimizer class (rename file and update type). Update generator and parser/writer call sites to use PackedRepeatedOptimizer. Add NETSTANDARD2_0-safe CreateSpan implementation and switch to itemReader.ParseMessageFrom calls. Update PublicAPI.Unshipped lists to reflect the new API surface. Refactor-only and adds span compatibility for netstandard2.0; behavior unchanged. --- src/LightProto.Generator/Helper.cs | 4 +-- ...Repeated.cs => PackedRepeatedOptimizer.cs} | 32 ++++++++++++------- src/LightProto/Parser/Array.cs | 4 ++- src/LightProto/Parser/IEnumerableReader.cs | 2 +- src/LightProto/Parser/IEnumerableWriter.cs | 9 +++++- .../PublicAPI/net/PublicAPI.Unshipped.txt | 5 +-- .../netstandard2.0/PublicAPI.Unshipped.txt | 5 +-- 7 files changed, 41 insertions(+), 20 deletions(-) rename src/LightProto/{Serializer.PackedRepeated.cs => PackedRepeatedOptimizer.cs} (88%) diff --git a/src/LightProto.Generator/Helper.cs b/src/LightProto.Generator/Helper.cs index cb68e6d..346e1e7 100644 --- a/src/LightProto.Generator/Helper.cs +++ b/src/LightProto.Generator/Helper.cs @@ -681,7 +681,7 @@ private static void GenerateInlineArrayProtoWriter(CodeWriter writer, InlineArra writer.WriteLine("output.WriteTag(Tag);"); writer.WriteLine("output.WriteLongLength(size);"); writer.WriteLine( - "if (global::LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref output, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize))" + "if (global::LightProto.PackedRepeatedOptimizer.TryWritePackedRepeatedFieldLittleEndian(ref output, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize))" ); using (writer.IndentScope()) { @@ -746,7 +746,7 @@ private static void GenerateInlineArrayProtoReader(CodeWriter writer, InlineArra { writer.WriteLine($"var collection = default({inlineArrayType});"); writer.WriteLine( - "global::LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref input, ItemReader, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize);" + "global::LightProto.PackedRepeatedOptimizer.ParseRepeatedFieldIntoSpan(ref input, ItemReader, global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref collection[0], Length), ItemFixedSize);" ); writer.WriteLine("return collection;"); } diff --git a/src/LightProto/Serializer.PackedRepeated.cs b/src/LightProto/PackedRepeatedOptimizer.cs similarity index 88% rename from src/LightProto/Serializer.PackedRepeated.cs rename to src/LightProto/PackedRepeatedOptimizer.cs index cbf8769..031e75f 100644 --- a/src/LightProto/Serializer.PackedRepeated.cs +++ b/src/LightProto/PackedRepeatedOptimizer.cs @@ -4,7 +4,7 @@ namespace LightProto { - public static partial class Serializer + public static class PackedRepeatedOptimizer { [EditorBrowsable(EditorBrowsableState.Never)] public static bool TryWritePackedRepeatedFieldLittleEndian(ref WriterContext output, ReadOnlySpan values, int itemFixedSize) @@ -54,7 +54,7 @@ int itemFixedSize while (!SegmentedBufferHelper.IsReachedLimit(ref input.state)) { - var item = ParseMessageFrom(itemReader, ref input); + var item = itemReader.ParseMessageFrom(ref input); if (writtenCount < destination.Length) { destination[writtenCount] = item; @@ -73,7 +73,7 @@ int itemFixedSize do { - var item = ParseMessageFrom(itemReader, ref input); + var item = itemReader.ParseMessageFrom(ref input); if (writtenCount < destination.Length) { destination[writtenCount] = item; @@ -168,10 +168,7 @@ private static bool TryGetBytes(ReadOnlySpan values, int itemFixedSize, ou return true; } - bytes = MemoryMarshal.CreateSpan( - ref Unsafe.As(ref MemoryMarshal.GetReference(values)), - checked(values.Length * itemFixedSize) - ); + bytes = CreateSpan(ref Unsafe.As(ref MemoryMarshal.GetReference(values)), checked(values.Length * itemFixedSize)); return true; } @@ -189,13 +186,26 @@ private static bool TryGetBytes(Span values, int itemFixedSize, out Span(ref MemoryMarshal.GetReference(values)), - checked(values.Length * itemFixedSize) - ); + bytes = CreateSpan(ref Unsafe.As(ref MemoryMarshal.GetReference(values)), checked(values.Length * itemFixedSize)); return true; } +#if NETSTANDARD2_0 + private static unsafe Span CreateSpan(ref T reference, int length) + where T : unmanaged + { + fixed (T* ptr = &reference) + { + return new Span(ptr, length); + } + } +#else + private static Span CreateSpan(ref T reference, int length) + { + return MemoryMarshal.CreateSpan(ref reference, length); + } +#endif + private static bool CanUseLittleEndianPackedMemoryCopy(int itemFixedSize) { if (!BitConverter.IsLittleEndian || itemFixedSize <= 0) diff --git a/src/LightProto/Parser/Array.cs b/src/LightProto/Parser/Array.cs index aa4fddf..2669862 100644 --- a/src/LightProto/Parser/Array.cs +++ b/src/LightProto/Parser/Array.cs @@ -56,7 +56,9 @@ public TItem[] ParseFrom(ref ReaderContext ctx) { var count = length / fixedSize; var collection = new TItem[count]; - if (!Serializer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, collection.AsSpan(), fixedSize)) + if ( + !PackedRepeatedOptimizer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, collection.AsSpan(), fixedSize) + ) { int i = 0; while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state)) diff --git a/src/LightProto/Parser/IEnumerableReader.cs b/src/LightProto/Parser/IEnumerableReader.cs index aada691..a6da9fd 100644 --- a/src/LightProto/Parser/IEnumerableReader.cs +++ b/src/LightProto/Parser/IEnumerableReader.cs @@ -79,7 +79,7 @@ public TCollection ParseFrom(ref ReaderContext ctx) #if NET8_0_OR_GREATER if ( collection is List list - && Serializer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, list, (int)count, fixedSize) + && PackedRepeatedOptimizer.TryReadPackedRepeatedFieldLittleEndian(ref ctx, length, list, (int)count, fixedSize) ) { return collection; diff --git a/src/LightProto/Parser/IEnumerableWriter.cs b/src/LightProto/Parser/IEnumerableWriter.cs index f63d421..f997694 100644 --- a/src/LightProto/Parser/IEnumerableWriter.cs +++ b/src/LightProto/Parser/IEnumerableWriter.cs @@ -124,7 +124,14 @@ public void WriteTo(ref WriterContext output, TCollection collection) output.WriteTag(Tag); output.WriteLongLength(size); - if (Serializer.TryWritePackedRepeatedFieldLittleEndian(ref output, collection, count, ItemFixedSize)) + if ( + PackedRepeatedOptimizer.TryWritePackedRepeatedFieldLittleEndian( + ref output, + collection, + count, + ItemFixedSize + ) + ) { return; } diff --git a/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt b/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt index 977a239..2904c4b 100644 --- a/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/LightProto/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable LightProto.IProtoWriter.CalculateLongSize(object! value) -> long LightProto.IProtoWriter.CalculateLongSize(T value) -> long +LightProto.PackedRepeatedOptimizer LightProto.Parser.ICollectionReader.Empty.get -> object! LightProto.Parser.ICollectionWriter.ItemWireType.get -> LightProto.WireFormat.WireType LightProto.Parser.ICollectionWriter.Tag.get -> uint @@ -15,8 +16,8 @@ static LightProto.CodedOutputStream.ComputeLongLengthSize(long length) -> int static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, object! value) -> long static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, T value) -> long static LightProto.Serializer.CalculateLongSize(T message) -> long -static LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int -static LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool +static LightProto.PackedRepeatedOptimizer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int +static LightProto.PackedRepeatedOptimizer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool static LightProto.InvalidProtocolBufferException.MissingRequiredMember(string! memberName) -> LightProto.InvalidProtocolBufferException! LightProto.Parser.ImmutableQueueProtoReader LightProto.Parser.ImmutableQueueProtoReader.Empty.get -> System.Collections.Immutable.ImmutableQueue! diff --git a/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index c8ed12e..cc7beb4 100644 --- a/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/LightProto/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable LightProto.IProtoWriter.CalculateLongSize(object! value) -> long LightProto.IProtoWriter.CalculateLongSize(T value) -> long +LightProto.PackedRepeatedOptimizer LightProto.Parser.ICollectionReader.Empty.get -> object! LightProto.Parser.ICollectionWriter.ItemWireType.get -> LightProto.WireFormat.WireType LightProto.Parser.ICollectionWriter.Tag.get -> uint @@ -14,8 +15,8 @@ LightProto.WriterContext.WriteLongLength(long length) -> void static LightProto.CodedOutputStream.ComputeLongLengthSize(long length) -> int static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, object! value) -> long static LightProto.Serializer.CalculateLongMessageSize(this LightProto.IProtoWriter! writer, T value) -> long -static LightProto.Serializer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int -static LightProto.Serializer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool +static LightProto.PackedRepeatedOptimizer.ParseRepeatedFieldIntoSpan(ref LightProto.ReaderContext input, LightProto.IProtoReader! itemReader, System.Span destination, int itemFixedSize) -> int +static LightProto.PackedRepeatedOptimizer.TryWritePackedRepeatedFieldLittleEndian(ref LightProto.WriterContext output, System.ReadOnlySpan values, int itemFixedSize) -> bool static LightProto.InvalidProtocolBufferException.MissingRequiredMember(string! memberName) -> LightProto.InvalidProtocolBufferException! LightProto.Parser.ImmutableQueueProtoReader LightProto.Parser.ImmutableQueueProtoReader.Empty.get -> System.Collections.Immutable.ImmutableQueue! From 9f4f4497d049d90f274a903e8bb89c7b503f1f03 Mon Sep 17 00:00:00 2001 From: Dameng <313880747@qq.com> Date: Thu, 2 Jul 2026 20:08:46 +0800 Subject: [PATCH 3/4] add FixedSizeInlineArrayTests --- tests/LightProto.Tests/Parsers/InlineArray.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/LightProto.Tests/Parsers/InlineArray.cs b/tests/LightProto.Tests/Parsers/InlineArray.cs index d09a582..88f23be 100644 --- a/tests/LightProto.Tests/Parsers/InlineArray.cs +++ b/tests/LightProto.Tests/Parsers/InlineArray.cs @@ -113,6 +113,85 @@ await Assert } } +[InheritsTests] +public partial class FixedSizeInlineArrayTests : BaseTests +{ + [ProtoContract] + [ProtoBuf.ProtoContract] + public partial class Message + { + [ProtoMember(1)] + [ProtoBuf.ProtoMember(1, DataFormat = ProtoBuf.DataFormat.FixedSize)] + public IntInlineArray10 Property { get; set; } = new(); + + public override string ToString() + { + return $"Property: {string.Join(", ", InlineArray10ToEnumerable(Property))}"; + } + } + + protected override bool ProtoBuf_net_Serialize_Disabled => true; + + protected override bool ProtoBuf_net_Deserialize_Disabled => true; + + public IEnumerable GetIntArrays() + { + yield return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + yield return [-1, -2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + yield return [-1, -2, -3, -4, -5]; + yield return [0, 0, 0, 0, 0]; + yield return [0]; + yield return []; + } + + public override IEnumerable GetMessages() + { + return GetIntArrays().Select(x => new Message() { Property = FillInlineArray10(x) }); + } + + static IntInlineArray10 FillInlineArray10(int[] array) + { + var inlineArray = new IntInlineArray10(); + for (int i = 0; i < 10; i++) + { + if (i < array.Length) + { + inlineArray[i] = array[i]; + } + } + return inlineArray; + } + + static IEnumerable InlineArray10ToEnumerable(IntInlineArray10 inlineArray10) + { + for (var index = 0; index < 10; index++) + { + yield return inlineArray10[index]; + } + } + + public override IEnumerable GetGoogleMessages() + { + return GetIntArrays() + .Select(o => + { + return new FixedSizeArrayTestsMessage() { Property = { InlineArray10ToEnumerable(FillInlineArray10(o)) } }; + }); + } + + public override async Task AssertGoogleResult(FixedSizeArrayTestsMessage clone, Message message) + { + await Assert.That(clone.Property.ToArray()).IsEquivalentTo(InlineArray10ToEnumerable(message.Property).ToArray()); + } + + public override async Task AssertResult(Message clone, Message message) + { + await Assert + .That(InlineArray10ToEnumerable(clone.Property).ToArray()) + .IsEquivalentTo(InlineArray10ToEnumerable(message.Property).ToArray()); + } +} + [InlineArray(4)] public struct GenericInlineArray { From 198147eca93388c8791604ea48dff9f3a59ee0ad Mon Sep 17 00:00:00 2001 From: Dameng <313880747@qq.com> Date: Thu, 2 Jul 2026 21:05:30 +0800 Subject: [PATCH 4/4] Update InlineArray.cs --- tests/LightProto.Tests/Parsers/InlineArray.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LightProto.Tests/Parsers/InlineArray.cs b/tests/LightProto.Tests/Parsers/InlineArray.cs index 88f23be..58b9fcc 100644 --- a/tests/LightProto.Tests/Parsers/InlineArray.cs +++ b/tests/LightProto.Tests/Parsers/InlineArray.cs @@ -120,8 +120,8 @@ public partial class FixedSizeInlineArrayTests : BaseTests