Skip to content

Defer CBOR reader strict-map key tracking until second key - #131050

Closed
artl93 wants to merge 1 commit into
dotnet:mainfrom
artl93:artl93-cbor-reader-map-optimization
Closed

artl93 wants to merge 1 commit into
dotnet:mainfrom
artl93:artl93-cbor-reader-map-optimization

Conversation

@artl93

@artl93 artl93 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Defers the HashSet<(int,int)> allocation used for strict-mode map key-uniqueness validation until a second key is actually encountered. Single-pair strict maps (a common shape) no longer allocate the set at all.

What changed

CborReader.ValidateKeyUniqueness previously allocated (or rented) the key-encoding HashSet on the very first key of every strict-mode map. Now the first key's encoding range is stashed in the existing _previousKeyEncodingRange slot; the set is only allocated on the second key, seeded with the stashed first range before the duplicate check proceeds. Behavior is otherwise identical — duplicate detection, buffer reset on throw, checkpoint rollback, and nested/pooled state are all unchanged.

The _previousKeyEncodingRange field already exists and is used by the canonical-mode sorted-key path. Strict and canonical validation are mutually exclusive for a given reader (fixed by ConformanceMode), so the two never share the slot concurrently.

Results

Local BenchmarkDotNet A/B (macOS arm64, .NET 11.0.0, BDN 0.14.0, [MemoryDiagnoser]), same source except this one file:

Method Baseline Alloc This PR Alloc
OnePair_Strict_Fresh 712 B 424 B
OnePair_Strict_Reused 0 B 0 B
Larger_Strict (32 pairs) 2088 B 2088 B
Canonical_Sorted 424 B 424 B
Nested_Strict 1240 B 1240 B
Duplicate_Strict 1288 B 1288 B
Rollback_Strict 1312 B 1312 B

One-pair strict maps drop from 712 B → 424 B (−40%), now matching the canonical path that never needed the set. All multi-key/canonical/nested/duplicate/rollback controls are byte-identical (no regression); the pooled reused-reader path stays at 0 B.

Full System.Formats.Cbor test suite passes locally (1921 total, 0 failed, 0 skipped).

Note

This PR description was drafted with the assistance of GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9c75d62-dda8-4115-be1b-ef2f101a6d61
Copilot AI review requested due to automatic review settings July 20, 2026 00:27
@artl93

artl93 commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

@EgorBot -amd -osx_arm64

using System.Formats.Cbor;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class CborReaderMapBenchmarks
{
    private byte[] _onePair = null!;
    private byte[] _larger = null!;
    private byte[] _canonical = null!;
    private byte[] _nested = null!;
    private static readonly byte[] s_duplicate = new byte[] { 0xa2, 0x01, 0x00, 0x01, 0x00 };
    private CborReader _reused = null!;

    [GlobalSetup]
    public void Setup()
    {
        _onePair = EncodeIntMap(CborConformanceMode.Strict, pairs: 1);
        _larger = EncodeIntMap(CborConformanceMode.Strict, pairs: 32);
        _canonical = EncodeIntMap(CborConformanceMode.Canonical, pairs: 32);
        _nested = EncodeNested(CborConformanceMode.Strict, depth: 4, keysPerLevel: 2);
        _reused = new CborReader(_onePair, CborConformanceMode.Strict);
    }

    [Benchmark(Baseline = true)]
    public int OnePair_Strict_Fresh() => ReadValue(new CborReader(_onePair, CborConformanceMode.Strict));

    [Benchmark]
    public int OnePair_Strict_Reused()
    {
        _reused.Reset(_onePair);
        return ReadValue(_reused);
    }

    [Benchmark]
    public int Larger_Strict() => ReadValue(new CborReader(_larger, CborConformanceMode.Strict));

    [Benchmark]
    public int Canonical_Sorted() => ReadValue(new CborReader(_canonical, CborConformanceMode.Canonical));

    [Benchmark]
    public int Nested_Strict() => ReadValue(new CborReader(_nested, CborConformanceMode.Strict));

    [Benchmark]
    public int Duplicate_Strict()
    {
        var reader = new CborReader(s_duplicate, CborConformanceMode.Strict);
        try { return ReadValue(reader); }
        catch (CborContentException) { return -1; }
    }

    [Benchmark]
    public int Rollback_Strict()
    {
        var reader = new CborReader(s_duplicate, CborConformanceMode.Strict);
        try { reader.SkipValue(); return 0; }
        catch (CborContentException) { return -1; }
    }

    private static int ReadValue(CborReader reader)
    {
        if (reader.PeekState() == CborReaderState.StartMap)
        {
            int count = reader.ReadStartMap()!.Value;
            int sum = 0;
            for (int i = 0; i < count; i++)
            {
                sum += ReadValue(reader);
                sum += ReadValue(reader);
            }
            reader.ReadEndMap();
            return sum;
        }

        return reader.ReadInt32();
    }

    private static byte[] EncodeIntMap(CborConformanceMode mode, int pairs)
    {
        var writer = new CborWriter(mode);
        writer.WriteStartMap(pairs);
        for (int i = 0; i < pairs; i++)
        {
            writer.WriteInt32(i);
            writer.WriteInt32(i);
        }
        writer.WriteEndMap();
        return writer.Encode();
    }

    private static byte[] EncodeNested(CborConformanceMode mode, int depth, int keysPerLevel)
    {
        var writer = new CborWriter(mode);
        WriteNested(writer, depth, keysPerLevel);
        return writer.Encode();
    }

    private static void WriteNested(CborWriter writer, int depth, int keysPerLevel)
    {
        if (depth == 0)
        {
            writer.WriteInt32(0);
            return;
        }

        writer.WriteStartMap(keysPerLevel);
        for (int i = 0; i < keysPerLevel; i++)
        {
            writer.WriteInt32(i);
            WriteNested(writer, depth - 1, keysPerLevel);
        }
        writer.WriteEndMap();
    }
}

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.
Some pipeline(s) encountered errors during trigger evaluation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Optimizes strict-mode CBOR map key uniqueness validation by deferring the HashSet<(int,int)> allocation until a second key is encountered, avoiding allocations for single-entry strict maps.

Changes:

  • Avoid allocating/renting the key-tracking HashSet on the first strict map key by stashing the first key’s encoding range in _previousKeyEncodingRange.
  • Allocate and seed the HashSet only when a second key is read, then proceed with duplicate detection as before.
Show a summary per file
File Description
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/Reader/CborReader.Map.cs Defers strict-mode key-uniqueness HashSet creation until the second key and seeds it with the first key’s range.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment on lines +163 to +174
HashSet<(int Offset, int Length)>? keyEncodingRanges = _keyEncodingRanges;
if (keyEncodingRanges is null)
{
if (_previousKeyEncodingRange is null)
{
_previousKeyEncodingRange = currentKeyEncodingRange;
return;
}

keyEncodingRanges = GetKeyEncodingRanges();
keyEncodingRanges.Add(_previousKeyEncodingRange.Value);
}
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-cbor, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants