protocolbuffers / protocolbuffers/protobuf
C#: packed repeated uint32 lacks the preallocation present in the C++ codec
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 72k
- Forks
- 16.3k
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 140
Description
What language does this apply to?
C# / .NET
Describe the problem you are trying to solve.
C# is lacking pre-allocation when dealing with repeated uint32, leading to 3.25x memory allocations on parsing, and on serialisation the encoded length is computed twice, each pass invoking a delegate per element.
Describe the solution you'd like
Count terminator bytes, preallocate exactly; drop the delegate from the size pass.
Describe alternatives you've considered
Caching the size like C++ (rejected — RepeatedField is publicly mutable); the deeper FieldCodec refactor
Additional context
When parsing repeated uint32 values, there is an optimisation present in the C++ library where CountVarintsAssumingLargeArray in parse_context.cc counts bytes with the continuation bit clear using a popcount over 64-bit blocks; ReadPackedVarintWithField exists specifically to take the field so the result can be preallocated.
Applying the same technique gives, at 10,000 elements (BenchmarkDotNet,
.NET 10, ns per value):
uniform random 12.38 -> 4.96 2.50x
values < 16384 7.61 -> 2.11 3.60x
values < 128 6.70 -> 1.41 4.76x
On the serialisation side, there is still a large gap between the reference C++ codec and the C# one, but we can take a chunk out of this difference too.
Serializing a packed field needs its encoded length twice: once from CalculateSize to size the output buffer, and again inside RepeatedField.WriteTo to emit the length prefix. Neither is cached, and for a varint field each pass calls the codec ValueSizeCalculator delegate once per element.
Measuring WriteTo ( ns per element ) :
uniform random 14.59 -> 12.04 17.5%
values < 16384 7.49 -> 5.16 31.2%
values < 128 5.47 -> 3.23 41.1%
To get to performance parity requires a deeper refactor. Every element dispatches through the codec's ValueWriter delegate; closing it would mean generating per-field write loops rather than going through FieldCodec, which is a design change rather than a patch.
I've had to write and use my own BenchmarkDotNet benchmarks, as I couldn't see any existing benchmarks in this repo for the C# / .NET implementation specifically.
I have local branches which pass tests to address each of these, I'd be happy to draft PRs for them. I couldn't see an AI policy or agents.md, I used claude to assist with these changes and generation of benchmarks.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the C#/.NET repeated uint32 parsing and serialization paths, then compare them with CountVarintsAssumingLargeArray in parse_context.cc. Read ReadPackedVarintWithField, RepeatedField, and FieldCodec to understand the existing allocation and size-calculation flow. Done means the optimizations pass the existing tests and reproduce the reported allocation and BenchmarkDotNet improvements.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, csharp
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100