Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash - #126700

Closed
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash
Closed

Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash#126700
EgorBo wants to merge 12 commits into
dotnet:mainfrom
EgorBo:opt-IndexOfQuoteOrAnyControlOrBackSlash

Conversation

@EgorBo

@EgorBoEgorBo commented Apr 9, 2026

Copy link
Copy Markdown
Member

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:IndexOfQuoteOrAnyControlOrBackSlashG_M1629_IG01: stp fp, lr,[sp, #-0x20]!str x19,[sp, #0x18]mov fp,spG_M1629_IG02:cmp w1, #16 blt G_M1629_IG07G_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_IG05add x0, x0, #16sub w1, w1, #16 movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2] blr x2add w1, w0, #16cmp w0, #0 csel w0, w1, w0, geG_M1629_IG04: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG05: rbit x0, x19 clz x0, x0 asr w0, w0, #2G_M1629_IG06:  ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20ret lrG_M1629_IG07: movz x2, #0x67F0 // code for JsonReaderHelper:IndexOfQuoteOrAnyControlOrBackSlash_Fallback(System.ReadOnlySpan`1[byte]):int movk x2, #0xC4E4LSL #16 movk x2, #0x7FFBLSL #32 ldr x2,[x2]G_M1629_IG08: ldr x19,[sp, #0x18] ldp fp, lr,[sp], #0x20 br x2

CopilotAI 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.

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 11:59
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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
MemberAuthor

@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.

CopilotAI review requested due to automatic review settings April 9, 2026 17:12

CopilotAI 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

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>
CopilotAI review requested due to automatic review settings April 9, 2026 17:46

CopilotAI 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

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

CopilotAI review requested due to automatic review settings April 9, 2026 19:48

CopilotAI 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

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

Removed experimental SVE code for finding index of quote or control characters.
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@tannergooding do we need anything else here? I am cooking a PR to intrinsify IndexOfWhereAllBitsSet in JIT atm, so if it goes well, I'll replace this code with that

@tannergooding

Copy link
Copy Markdown
Member

This LGTM to me as a fast path optimization. We can cleanup more as JIT optimizations come online and if we improve SearchValues so that it can be used directly after the appropriate slice occurs.

We probably want weigh-in from @eiriktsarpalis as well to ensure that 16 is the "right" size and not just a size that's optimal for this particular first party scenario (also cc. @jeffhandley as an fyi).

CopilotAI review requested due to automatic review settings April 13, 2026 15:41
@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

CopilotAI 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

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

Comment on lines +5 to +8
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
Comment on lines +39 to +41
if (cmp != Vector128<byte>.Zero)
{
ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask) >> 2;
}
else
{
uint mask = cmp.ExtractMostSignificantBits();
if (mask != 0)
return BitOperations.TrailingZeroCount(mask);
return Vector128.IndexOfWhereAllBitsSet(cmp);

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.

Given we're expecting a match within the first vector, is this "better" than:

intindex=Vector128.IndexOfWhereAllBitsSet(cmp);if(index>=0){returnindex;}

I'd imagine its generating an extra test/extract for a case we presume is unlikely.

@EgorBoEgorBoApr 13, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I can test with the both options. I think last time I checked cmp != Vector128<byte>.Zero didn't add overhead, but it might reduce it if there is no match in the first vector

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@EgorBot -linux_azure_arm -arm -linux_aws_arm -profiler

usingSystem.Text;usingSystem.Text.Json;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);[MemoryDiagnoser]publicclassBenchmarks{// ── TokenSerialization fields ────────────────────────────────────────────privateList<object>_tokenObjects;[ThreadStatic]staticUtf8JsonWritert_writer;[ThreadStatic]staticMemoryStreamt_stream;[GlobalSetup]publicvoidSetup(){// TokenSerialization_tokenObjects=newList<object>(200);for(inti=0;i<200;i++){if(i%3==0)_tokenObjects.Add(GenerateRecordJson(1));else_tokenObjects.Add(newDictionary<string,object>{["seq"]=i,["label"]=$"item_{i}",["blob"]=newbyte[100]});}}privatestaticstringGenerateRecordJson(inttargetSizeKb=150){varsb=newStringBuilder(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\":[");inttargetBytes=targetSizeKb*1024;intfieldIndex=0;boolfirstField=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("}");returnsb.ToString();}[Benchmark]publicvoidTokenSerialization(){varstream=t_stream??=newMemoryStream(64*1024);stream.Position=0;stream.SetLength(0);varwriter=t_writer;if(writer==null){writer=newUtf8JsonWriter(stream,newJsonWriterOptions{SkipValidation=true});t_writer=writer;}elsewriter.Reset(stream);writer.WriteStartObject();writer.WriteStartArray("Catalog");foreach(vartokenin_tokenObjects){if(tokenisstringstrToken){if(!string.IsNullOrEmpty(strToken))writer.WriteRawValue(strToken);}elseif(tokenisDictionary<string,object>dictToken){writer.WriteStartObject();foreach(varkvpindictToken){writer.WritePropertyName(kvp.Key);JsonSerializer.Serialize(writer,kvp.Value);}writer.WriteEndObject();}}writer.WriteEndArray();writer.WriteEndObject();writer.Flush();if(stream.Length==0)thrownewException("unreachable");}}

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Seems like after #126678 this PR no longer produces clear wins (suprisingly). It seems slow EMSB was the problem. Closing.

@EgorBoEgorBo closed this Apr 13, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@tannergooding