Skip to content

Eliminate avoidable string allocations in JsonReader when parsing common property names #3352

@gathogojr

Description

@gathogojr

Is your feature request related to a problem? Please describe.

We currently allocation strings when parsing common property names

Describe the solution you'd like

Eliminate avoidable string allocations in JsonReader when parsing common property names (e.g., "@odata.context"/"@context", "@odata.type"/"@type", "@odata.count"/"@count", "@odata.id"/"@id", "value" - maybe even "id"/"Id", "name"/"Name")

private const string IdCamelCasePropertyName = "id";
private const string IdPascalCasePropertyName = "Id";

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool Is(ReadOnlySpan<char> s, ReadOnlySpan<char> expect) =>
    s.Length == expect.Length && s.SequenceEqual(expect);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryInternCommonPropertyName(ReadOnlySpan<char> span, out string value)
{
    switch (span.Length)
    {
        case 2: // id / Id
            if (span[0] == 'i' && span[1] == 'd') { value = IdCamelCasePropertyName; return true; }
            if (span[0] == 'I' && span[1] == 'd') { value = IdPascalCasePropertyName; return true; }
            break;

        case 3: // @id
            if (Is(span, SimplifiedODataIdPropertyName)) { value = SimplifiedODataIdPropertyName; return true; }
            break;

        case 5: // value, @type
            if (Is(span, ODataValuePropertyName)) { value = ODataValuePropertyName; return true; }
            if (Is(span, SimplifiedODataTypePropertyName)) { value = SimplifiedODataTypePropertyName; return true; }
            break;

        case 8: // @context
            if (Is(span, SimplifiedODataContextPropertyName)) { value = SimplifiedODataContextPropertyName; return true; }
            break;

        case 9: // @odata.id
            if (Is(span, PrefixedODataIdPropertyName)) { value = PrefixedODataIdPropertyName; return true; }
            break;

        case 11: // @odata.type
            if (Is(span, PrefixedODataTypePropertyName)) { value = PrefixedODataTypePropertyName; return true; }
            break;

        case 14: // @odata.context
            if (Is(span, PrefixedODataContextPropertyName)) { value = PrefixedODataContextPropertyName; return true; }
            break;

        // ... other common property names
    }

    value = null;
    return false;
}

Then in ParseName:

private string ParseName()
{
    Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "Must have at least one character available.");

    char firstCharacter = this.characterBuffer[this.tokenStartIndex];
    if (firstCharacter == '"' || firstCharacter == '\'')
    {
        return this.ParseStringPrimitiveValue();
    }

    int currentCharacterTokenRelativeIndex = 0;
    do
    {
        char c = this.characterBuffer[this.tokenStartIndex + currentCharacterTokenRelativeIndex];
        if (IsCharacterAllowedInPropertyName(c))
        {
            currentCharacterTokenRelativeIndex++;
        }
        else
        {
            break;
        }
    }
    while ((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount || this.ReadInput());

    ReadOnlySpan<char> span = this.characterBuffer.AsSpan(this.tokenStartIndex, currentCharacterTokenRelativeIndex);

    if (TryInternCommonPropertyName(span, out string interned))
    {
        this.tokenStartIndex += currentCharacterTokenRelativeIndex;
        return interned; // return shared instance (no allocation)
    }

    return this.ConsumeTokenToString(currentCharacterTokenRelativeIndex);
}

NOTE: To ensure this change delivers measurable improvements, benchmarking is essential to validate its impact on allocations and runtime performance.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions