Update IMessage.MergeFrom and MessageParser.ParseFrom to use zero-allocation spans for byte arrays
I was developing an application that caches protobuf messages serialized as byte arrays when I began looking into the various ways of deserializing those messages and noticed a discrepancy between the ReadOnlySpan<byte> implementation vs the byte[] implementation. This PR fixes that discrepancy by backing the byte[] and ByteString deserializers with the new zero-allocation ReadOnlySpan<byte> code path (introduced in #8473) rather than the CodedInputStream code path.
I wrote a small benchmark (not included in this PR but it can be if there is interest) to demonstrate the small perf win. Note that ByteArray still takes slightly longer than Span due to more aggressive inlining when using the span methods.
[MemoryDiagnoser]
public class MergeFromBytes
{
public static readonly byte[] Serialized = new TestAllTypes
{
SingleBool = true,
SingleBytes = ByteString.CopyFrom(1, 2, 3, 4),
SingleDouble = 23.5,
SingleFixed32 = 23,
SingleFixed64 = 1234567890123,
SingleFloat = 12.25f,
SingleForeignEnum = ForeignEnum.ForeignBar,
SingleForeignMessage = new ForeignMessage { C = 10 },
SingleImportEnum = ImportEnum.ImportBaz,
SingleImportMessage = new ImportMessage { D = 20 },
SingleInt32 = 100,
SingleInt64 = 3210987654321,
SingleNestedEnum = TestAllTypes.Types.NestedEnum.Foo,
SingleNestedMessage = new TestAllTypes.Types.NestedMessage { Bb = 35 },
SinglePublicImportMessage = new PublicImportMessage { E = 54 },
SingleSfixed32 = -123,
SingleSfixed64 = -12345678901234,
SingleSint32 = -456,
SingleSint64 = -12345678901235,
SingleString = "test",
SingleUint32 = uint.MaxValue,
SingleUint64 = ulong.MaxValue
}.ToByteArray();
[Benchmark]
public TestAllTypes ByteArray() => TestAllTypes.Parser.ParseFrom(Serialized);
[Benchmark]
public TestAllTypes CodedInputStream() => TestAllTypes.Parser.ParseFrom(new CodedInputStream(Serialized));
[Benchmark]
public TestAllTypes Span() => TestAllTypes.Parser.ParseFrom(new ReadOnlySpan<byte>(Serialized));
}
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.963)
AMD Ryzen 7 2700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.404
[Host] : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
| ByteArray | 683.6 ns | 1.59 ns | 1.41 ns | 0.3052 | 1.25 KB |
| CodedInputStream | 748.8 ns | 2.27 ns | 2.01 ns | 0.3462 | 1.41 KB |
| Span | 663.7 ns | 2.00 ns | 1.87 ns | 0.3052 | 1.25 KB |
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).
View this failed invocation of the CLA check for more information.
For the most up to date status, view the checks section at the bottom of the pull request.
We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active, please add a comment.
This PR is labeled inactive because the last activity was over 90 days ago. This PR will be closed and archived after 14 additional days without activity.
We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active or becomes active again, please reopen it.
This PR was closed and archived because there has been no new activity in the 14 days since the inactive label was added.