Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700

Open
EgorBo wants to merge 9 commits intodotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Open

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 9 commits intodotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented Apr 9, 2026

Validate the theory we came up with @tannergooding that we mostly find " character within first 16 bytes in this function (and the span is most of the time is bigger than 16 bytes) - e.g. the end of a property name

This doesn't replace #126678, just special cases for JSON where we indeed can assume something is usually found early.

Benchmark - 10-13% improvement on Cobalt100

arm64 codegen:

; Assembly listing for method JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash
G_M1629_IG01:
            stp     fp, lr, [sp, #-0x20]!
            str     x19, [sp, #0x18]
            mov     fp, sp
G_M1629_IG02:
            cmp     w1, #16
            blt     G_M1629_IG07
G_M1629_IG03:
            ldr     q16, [x0]
            movi    v17.16b, #0x20
            cmhi    v17.16b, v17.16b, v16.16b
            movi    v18.16b, #0x22
            cmeq    v18.16b, v16.16b, v18.16b
            orr     v17.16b, v17.16b, v18.16b
            movi    v18.16b, #0x5C
            cmeq    v16.16b, v16.16b, v18.16b
            orr     v16.16b, v17.16b, v16.16b
            shrn    v16.8b, v16.8h, #4
            umov    x19, v16.d[0]
            cbnz    x19, G_M1629_IG05
            add     x0, x0, #16
            sub     w1, w1, #16
            movz    x2, #0x67F0      // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int
            movk    x2, #0xC4E4 LSL #16
            movk    x2, #0x7FFB LSL #32
            ldr     x2, [x2]
            blr     x2
            add     w1, w0, #16
            cmp     w0, #0
            csel    w0, w1, w0, ge
G_M1629_IG04:
            ldr     x19, [sp, #0x18]
            ldp     fp, lr, [sp], #0x20
            ret     lr
G_M1629_IG05:
            rbit    x0, x19
            clz     x0, x0
            asr     w0, w0, #2
G_M1629_IG06: 
            ldr     x19, [sp, #0x18]
            ldp     fp, lr, [sp], #0x20
            ret     lr
G_M1629_IG07:
            movz    x2, #0x67F0      // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int
            movk    x2, #0xC4E4 LSL #16
            movk    x2, #0x7FFB LSL #32
            ldr     x2, [x2]
G_M1629_IG08:
            ldr     x19, [sp, #0x18]
            ldp     fp, lr, [sp], #0x20
            br      x2

Copilot AI review requested due to automatic review settings April 9, 2026 11:41
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

@EgorBo

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a SIMD fast-path to JsonReaderHelper.IndexOfQuoteOrAnyControlOrBackSlash to quickly detect " / \ / control characters by scanning the first 16 bytes before falling back to SearchValues<byte>-based searching.

Changes:

  • Introduces a Vector128<byte>-based first-16-bytes scan for quote/backslash/control bytes.
  • Adds an ARM64-specific mask extraction path using AdvSimd plus BitOperations.TrailingZeroCount.
  • Moves the existing IndexOfAny(SearchValues<byte>) implementation into a non-inlined fallback helper.

…nReaderHelper.net8.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented Apr 9, 2026

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

using System.Text;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);

[MemoryDiagnoser]
public class Benchmarks
{
    // ── TokenSerialization fields ────────────────────────────────────────────
    private List<object> _tokenObjects;
    [ThreadStatic] static Utf8JsonWriter t_writer;
    [ThreadStatic] static MemoryStream t_stream;

    [GlobalSetup]
    public void Setup()
    {
        // TokenSerialization
        _tokenObjects = new List<object>(200);
        for (int i = 0; i < 200; i++)
        {
            if (i % 3 == 0)
                _tokenObjects.Add(GenerateRecordJson(1));
            else
                _tokenObjects.Add(new Dictionary<string, object>
                {
                    ["seq"] = i,
                    ["label"] = $"item_{i}",
                    ["blob"] = new byte[100]
                });
        }
    }

    private static string GenerateRecordJson(int targetSizeKb = 150)
    {
        var sb = new StringBuilder(targetSizeKb * 1024 + 512);
        sb.Append("{");
        sb.Append("\"TypeName\":\"product\",");
        sb.Append("\"CategoryCode\":1,");
        sb.Append("\"Label\":\"Product\",");
        sb.Append("\"IsAction\":false,");
        sb.Append("\"IsActionMember\":false,");
        sb.Append("\"IsTrackingEnabled\":true,");
        sb.Append("\"IsAvailableLocal\":true,");
        sb.Append("\"IsChildRecord\":false,");
        sb.Append("\"IsLinksEnabled\":true,");
        sb.Append("\"IsCustomRecord\":false,");
        sb.Append("\"PrimaryKeyField\":\"productid\",");
        sb.Append("\"PrimaryLabelField\":\"title\",");
        sb.Append("\"Fields\":[");
        int targetBytes = targetSizeKb * 1024;
        int fieldIndex = 0;
        bool firstField = true;
        while (sb.Length < targetBytes - 512)
        {
            if (!firstField) sb.Append(",");
            firstField = false;
            sb.Append("{");
            sb.Append($"\"TypeName\":\"field_{fieldIndex}\",");
            sb.Append($"\"InternalName\":\"Field_{fieldIndex}\",");
            sb.Append($"\"FieldType\":\"String\",");
            sb.Append($"\"Label\":\"Field {fieldIndex}\",");
            sb.Append($"\"MaxSize\":100,");
            sb.Append($"\"IsReadable\":true,");
            sb.Append($"\"IsCreatable\":true,");
            sb.Append($"\"IsUpdatable\":true,");
            sb.Append($"\"IsTrackingEnabled\":false,");
            sb.Append($"\"IsPrimaryKey\":false,");
            sb.Append($"\"IsVirtual\":false,");
            sb.Append($"\"Requirement\":\"None\"");
            sb.Append("}");
            fieldIndex++;
        }
        sb.Append("]");
        sb.Append("}");
        return sb.ToString();
    }

    [Benchmark]
    public void TokenSerialization()
    {
        var stream = t_stream ??= new MemoryStream(64 * 1024);
        stream.Position = 0;
        stream.SetLength(0);
        var writer = t_writer;
        if (writer == null)
        {
            writer = new Utf8JsonWriter(stream, new JsonWriterOptions { SkipValidation = true });
            t_writer = writer;
        }
        else
            writer.Reset(stream);
        writer.WriteStartObject();
        writer.WriteStartArray("Catalog");
        foreach (var token in _tokenObjects)
        {
            if (token is string strToken)
            {
                if (!string.IsNullOrEmpty(strToken))
                    writer.WriteRawValue(strToken);
            }
            else if (token is Dictionary<string, object> dictToken)
            {
                writer.WriteStartObject();
                foreach (var kvp in dictToken)
                {
                    writer.WritePropertyName(kvp.Key);
                    JsonSerializer.Serialize(writer, kvp.Value);
                }
                writer.WriteEndObject();
            }
        }
        writer.WriteEndArray();
        writer.WriteEndObject();
        writer.Flush();
        if (stream.Length == 0) throw new Exception("unreachable");
    }
}

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

@EgorBo EgorBo marked this pull request as ready for review April 9, 2026 12:42
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented Apr 9, 2026

@tannergooding I decided to implement the idea we discussed yesterday, I think your PR makes sense to check in too.
10-13% improvements on cloud arm (Cobalt100, Graviton4).

I couldn't detect more improvements from extending 16 bytes to 32 bytes so decided to keep as is. The fallback doesn't show up in the traces.

Copilot AI review requested due to automatic review settings April 9, 2026 17:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

…nReaderHelper.net8.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 9, 2026 17:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings April 9, 2026 19:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment on lines +39 to +43
ref byte searchSpace = ref MemoryMarshal.GetReference(span);
unsafe
{
fixed (byte* ptr = &searchSpace)
{
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

MemoryMarshal is used here but this file doesn't import System.Runtime.InteropServices (and there are no global usings), so this won't compile. Add using System.Runtime.InteropServices; at the top or fully-qualify MemoryMarshal at the usage site.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +38
if (Sve.IsSupported)
{
// SVE: Use predicated comparisons + brkb + cntp to find the index.
// The result flows through predicate registers directly to a scalar cntp —
// no SIMD-to-GP transfer (UMOV) is needed at all.
// On V2: cmpeq(4cy) + cmplo(4cy) + orr(1cy) + brkb(2cy) + cntp(2cy)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

In the Sve.IsSupported path, if there is no match in the first 16 bytes, execution falls through and repeats the same search using Vector128/AdvSimd before calling the fallback. Since the SVE check already proved there is no match in the first 16 bytes, consider returning directly to the fallback on span.Slice(Vector128<byte>.Count) to avoid the redundant work on SVE-capable machines.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +49
Vector<byte> combined = Sve.CompareEqual(data, new Vector<byte>((byte)'"'))
| Sve.CompareEqual(data, new Vector<byte>((byte)'\\'))
| Sve.CompareLessThan(data, new Vector<byte>((byte)0x20));
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The SVE path hardcodes ", \\, and 0x20 instead of using the existing JsonConstants.Quote / JsonConstants.BackSlash / JsonConstants.Space used later in the method. Using the shared constants would keep all paths consistent and avoid accidental divergence if the definitions ever change.

Suggested change
Vector<byte> combined = Sve.CompareEqual(data, new Vector<byte>((byte)'"'))
| Sve.CompareEqual(data, new Vector<byte>((byte)'\\'))
| Sve.CompareLessThan(data, new Vector<byte>((byte)0x20));
Vector<byte> combined = Sve.CompareEqual(data, new Vector<byte>(JsonConstants.Quote))
| Sve.CompareEqual(data, new Vector<byte>(JsonConstants.BackSlash))
| Sve.CompareLessThan(data, new Vector<byte>(JsonConstants.Space));

Copilot uses AI. Check for mistakes.
return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span);

#pragma warning disable SYSLIB5003 // SVE is experimental
if (Sve.IsSupported)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: If we're going to check this path in, it really needs to check Vector.IsHardwareAccelerated and span.Length >= Vector<byte>.Count so that it remains correct in the future as well.

Removed experimental SVE code for finding index of quote or control characters.
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