Repository navigation
Conversation
Route generic numeric UTF-8 Char parsing through the existing strict Rune-based parser. Reuse the sealed UTF-8 encoding's existing one-pass short-input decoder for span and validated array-slice GetString calls. Keep subclasses, custom encodings, and large inputs on their existing paths. BenchmarkDotNet 0.16.0-preview.1; Release CoreRun (.NET 12.0.0-dev), Windows x64, AMD Ryzen 9 7950X. Workstation GC (DOTNET_gcServer=0), one launch, three warmups, eight 250ms iterations. Old is the common base c0c1c33; new is the retained-candidate build measured locally. Benchmark: ```csharp using System; using System.Globalization; using System.IO; using System.Net; using System.Numerics; using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; BenchmarkSwitcher.FromAssembly(typeof(CharParsingBench).Assembly).Run(args); [MemoryDiagnoser] public class CharParsingBench { private byte[] _input = null!; [Params("A", "\u00E9")] public string Text { get; set; } = null!; [GlobalSetup] public void Setup() => _input = Encoding.UTF8.GetBytes(Text); [Benchmark] public char NumericUtf8Char() => Parse<char>(_input); private static T Parse<T>(ReadOnlySpan<byte> input) where T : struct, INumberBase<T> { if (!T.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out T result)) { throw new FormatException(); } return result; } } [MemoryDiagnoser] public class ShortUtf8Bench { private byte[] _input = null!; [Params("Content-Length: ", "Gr\u00FC\u00DFe\u4E16\u754C")] public string Text { get; set; } = null!; [GlobalSetup] public void Setup() => _input = Encoding.UTF8.GetBytes(Text); [Benchmark] public string SpanGetString() => Encoding.UTF8.GetString(_input.AsSpan()); [Benchmark] public string SliceGetString() => Encoding.UTF8.GetString(_input, 0, _input.Length); } ``` Results: | Benchmark | Input | Old (ns/op) | New (ns/op) | Old allocation | New allocation | |---|---|---:|---:|---:|---:| | NumericUtf8Char | A | 5.978 | 1.03 | 0 B | 0 B | | NumericUtf8Char | \xe9 | 6.741 | 1.399 | 0 B | 0 B | | SliceGetString | Content-Length: | 11.012 | 10.061 | 56 B | 56 B | | SliceGetString | Gr\xfc\xdfe\u4e16\u754c | 19.866 | 15.155 | 40 B | 40 B | | SpanGetString | Content-Length: | 9.92 | 9.822 | 56 B | 56 B | | SpanGetString | Gr\xfc\xdfe\u4e16\u754c | 18.404 | 12.724 | 40 B | 40 B | ASCII span decoding is unchanged within noise; short non-ASCII inputs improve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b1b2a80-4c3e-4179-adf6-89ba090b40d5
This comment was marked as resolved.
This comment was marked as resolved.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-text-encoding |
Member
Author
|
@EgorBot -arm -amd using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Numerics;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class CharParsingBench
{
private byte[] _input = null!;
[Params("A", "\u00E9")]
public string Text { get; set; } = null!;
[GlobalSetup]
public void Setup() => _input = Encoding.UTF8.GetBytes(Text);
[Benchmark]
public char NumericUtf8Char() => Parse<char>(_input);
private static T Parse<T>(ReadOnlySpan<byte> input) where T : struct, INumberBase<T>
{
if (!T.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out T result))
{
throw new FormatException();
}
return result;
}
}
[MemoryDiagnoser]
public class ShortUtf8Bench
{
private byte[] _input = null!;
[Params("Content-Length: ", "Gr\u00FC\u00DFe\u4E16\u754C")]
public string Text { get; set; } = null!;
[GlobalSetup]
public void Setup() => _input = Encoding.UTF8.GetBytes(Text);
[Benchmark]
public string SpanGetString() => Encoding.UTF8.GetString(_input.AsSpan());
[Benchmark]
public string SliceGetString() => Encoding.UTF8.GetString(_input, 0, _input.Length);
} |
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The new paths need targeted tests, and both conditions unnecessarily type-check inputs that exceed the fast-path threshold.
Review effort: Balanced
Findings: 1
Open (3)
What changed in this PR
Optimizes UTF-8 character parsing and short-string decoding by reusing existing direct parsers and decoders.
Changes:
- Routes generic
charUTF-8 parsing directly throughRune. - Reuses the sealed UTF-8 short-input decoder for span and array-slice calls.
- Preserves existing paths for subclasses and larger inputs.
| File | Description |
|---|---|
Char.cs |
Adds direct generic UTF-8 char parsing. |
Encoding.cs |
Adds the short-span UTF-8 fast path. |
UTF8Encoding.cs |
Adds the short array-slice fast path. |
UTF8Encoding.Sealed.cs |
Generalizes the existing short-input decoder to spans. |
|
|
||
| static bool INumberBase<char>.TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out char result) => TryParse(s, out result); | ||
|
|
||
| static bool INumberBase<char>.TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out char result) => TryParseUtf8(utf8Text, out result); |
|
|
||
| public unsafe string GetString(ReadOnlySpan<byte> bytes) | ||
| { | ||
| if (this is UTF8Encoding.UTF8EncodingSealed utf8Encoding && bytes.Length <= UTF8Encoding.UTF8EncodingSealed.MaxSmallInputElementCount) |
| if (count == 0) | ||
| return string.Empty; | ||
|
|
||
| if (this is UTF8EncodingSealed utf8Encoding && count <= UTF8EncodingSealed.MaxSmallInputElementCount) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Route generic numeric UTF-8 Char parsing through the existing strict Rune-based parser. Reuse the sealed UTF-8 encoding's existing one-pass short-input decoder for span and validated array-slice GetString calls. Keep subclasses, custom encodings, and large inputs on their existing paths.
Benchmark
Results from EgorBot/Benchmarks#652, comparing
mainagainst this PR. BenchmarkDotNet 0.16.0-preview.1; ARM64: Apple M4 / macOS 15.7.5; x64: AMD EPYC 9V45 / Ubuntu 24.04.4."A""é""Content-Length: ""Content-Length: ""Grüße世界""Grüße世界"Speedup is
main / PR. Char parsing is 3.59-5.35x faster; short-string decoding is 1.03-1.64x faster. Allocations are unchanged on both architectures: 0 B for Char parsing, 56 B for"Content-Length: ", and 40 B for"Grüße世界".