protocolbuffers / protocolbuffers/protobuf

C#: packed repeated uint32 lacks the preallocation present in the C++ codec

Open
#28,925 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

c# feature request
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.