Skip to content

Improve performance of barekey reading#109

Merged
prozolic merged 2 commits into
mainfrom
readbarekey
Jul 11, 2026
Merged

Improve performance of barekey reading#109
prozolic merged 2 commits into
mainfrom
readbarekey

Conversation

@prozolic

Copy link
Copy Markdown
Owner

This PR improves performance by changing a process based on Span<byte> tha checks byte by byte to a process using SIMD operations with SearchValues<byte>.

Benchmark

Method KeyLength Mean Error StdDev Ratio RatioSD BranchInstructions/Op BranchMispredictions/Op Code Size
SwitchLoop 8 9.952 ns 5.540 ns 0.3037 ns 1.00 0.04 46 0 NA
SwitchLoopStaticTable 8 9.275 ns 4.665 ns 0.2557 ns 0.93 0.03 46 0 NA
ClassifyTableLoop 8 6.356 ns 10.334 ns 0.5664 ns 0.64 0.05 20 0 110 B
SearchValuesIndexOf 8 5.574 ns 2.332 ns 0.1278 ns 0.56 0.02 10 0 605 B
DelimiterSearchThenValidate 8 8.291 ns 11.669 ns 0.6396 ns 0.83 0.06 10 0 829 B
SwitchLoop 32 45.163 ns 60.105 ns 3.2946 ns 1.00 0.09 166 1 NA
SwitchLoopStaticTable 32 44.581 ns 40.934 ns 2.2438 ns 0.99 0.08 167 0 NA
ClassifyTableLoop 32 20.422 ns 34.516 ns 1.8919 ns 0.45 0.05 68 0 110 B
SearchValuesIndexOf 32 5.942 ns 11.385 ns 0.6240 ns 0.13 0.01 13 0 601 B
DelimiterSearchThenValidate 32 8.199 ns 3.215 ns 0.1762 ns 0.18 0.01 14 0 875 B
Benchmark source
public class UnquotedKeyScan
{
    private const byte Tab = 0x09;
    private const byte Space = 0x20;
    private const byte Dot = 0x2E;
    private const byte Equal = 0x3D;

    private static readonly SearchValues<byte> BareKeyChars =
        SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8);

    private static readonly SearchValues<byte> KeyDelimiters =
        SearchValues.Create([Tab, Space, Dot, Equal]);

    // 0 = invalid, 1 = bare key char, 2 = delimiter
    private static readonly byte[] ClassTable = CreateClassTable();

    private static byte[] CreateClassTable()
    {
        var table = new byte[256];
        foreach (var b in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8)
        {
            table[b] = 1;
        }
        table[Tab] = 2;
        table[Space] = 2;
        table[Dot] = 2;
        table[Equal] = 2;
        return table;
    }

    private byte[] buffer = [];

    [Params(8, 32)]
    public int KeyLength { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        // realistic bare key content followed by " = 1234"
        ReadOnlySpan<byte> chars = "abcdefghijklmnopqrstuvwxyz_0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ"u8;
        ReadOnlySpan<byte> tail = " = 1234"u8;
        buffer = new byte[KeyLength + tail.Length];
        for (var i = 0; i < KeyLength; i++)
        {
            buffer[i] = chars[i % chars.Length];
        }
        tail.CopyTo(buffer.AsSpan(KeyLength));
    }

    // --- current implementation (CsTomlReader.ReadUnquotedString + TomlCodes.IsBareKey) ---

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsBareKey(byte rawByte)
    {
        ReadOnlySpan<bool> barekeyTable =
        [
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x00 - 0x0f
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x10 - 0x1f
            false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false,  // 0x20 - 0x2f
            true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false,           // 0x30 - 0x3f
            false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,                // 0x40 - 0x4f
            true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, true,             // 0x50 - 0x5f
            false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,                // 0x60 - 0x6f
            true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false,            // 0x70 - 0x7f
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x80 - 0x8f
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x90 - 0x9f
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xa0 - 0xaf
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xb0 - 0xbf
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xc0 - 0xcf
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xd0 - 0xdf
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xe0 - 0xef
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0xf0 - 0xff
        ];
        return Unsafe.Add(ref MemoryMarshal.GetReference(barekeyTable), rawByte);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Benchmark(Baseline = true)]
    public int SwitchLoop()
    {
        var span = buffer.AsSpan();
        for (var index = 0; index < span.Length; index++)
        {
            var ch = span[index];
            switch (ch)
            {
                case Tab:
                case Space:
                case Dot:
                case Equal:
                    return index;
                default:
                    if (!IsBareKey(ch))
                    {
                        ThrowInvalid(ch);
                    }
                    break;
            }
        }
        return span.Length;
    }

    private static readonly bool[] BareKeyTableArray = CreateBareKeyTableArray();

    private static bool[] CreateBareKeyTableArray()
    {
        var table = new bool[256];
        foreach (var b in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8)
        {
            table[b] = true;
        }
        return table;
    }

    // Same shape as SwitchLoop but the lookup is a static readonly bool[] instead of the
    // inlined RVA ReadOnlySpan<bool> local — probe for the DisassemblyDiagnoser NA issue.
    [MethodImpl(MethodImplOptions.NoInlining)]
    [Benchmark]
    public int SwitchLoopStaticTable()
    {
        var table = BareKeyTableArray;
        var span = buffer.AsSpan();
        for (var index = 0; index < span.Length; index++)
        {
            var ch = span[index];
            switch (ch)
            {
                case Tab:
                case Space:
                case Dot:
                case Equal:
                    return index;
                default:
                    if (!table[ch])
                    {
                        ThrowInvalid(ch);
                    }
                    break;
            }
        }
        return span.Length;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Benchmark]
    public int ClassifyTableLoop()
    {
        var table = ClassTable;
        var span = buffer.AsSpan();
        for (var i = 0; i < span.Length; i++)
        {
            var cls = table[span[i]];
            if (cls != 1)
            {
                if (cls == 2)
                {
                    return i;
                }
                ThrowInvalid(span[i]);
            }
        }
        return span.Length;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Benchmark]
    public int SearchValuesIndexOf()
    {
        var span = buffer.AsSpan();
        var index = span.IndexOfAnyExcept(BareKeyChars);
        if (index < 0)
        {
            return span.Length;
        }
        var terminator = span[index];
        if (terminator is Tab or Space or Dot or Equal)
        {
            return index;
        }
        ThrowInvalid(terminator);
        return -1;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [Benchmark]
    public int DelimiterSearchThenValidate()
    {
        var span = buffer.AsSpan();
        var index = span.IndexOfAny(KeyDelimiters);
        var keySpan = index < 0 ? span : span.Slice(0, index);
        if (keySpan.ContainsAnyExcept(BareKeyChars))
        {
            ThrowInvalid(0);
        }
        return index < 0 ? span.Length : index;
    }

    [DoesNotReturn]
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void ThrowInvalid(byte ch)
        => throw new InvalidOperationException($"Invalid bare key byte: 0x{ch:X2}");
}

This PR improves performance by changing a process based on Span<T>
tha checks byte by byte to a process using SIMD operations with SearchValues.
Copilot AI review requested due to automatic review settings July 11, 2026 01:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves unquoted/bare key parsing performance by replacing a byte-by-byte scan with a SIMD-friendly SearchValues<byte>-based vectorized scan, reducing branching in the hot path of key reading.

Changes:

  • Add TomlCodes.BareKeyChars (SearchValues<byte>) to represent the allowed bare key character set for vectorized scanning.
  • Update CsTomlReader.ReadUnquotedString to use IndexOfAnyExcept(TomlCodes.BareKeyChars) and then validate whether the first non-bare-key byte is a terminator or invalid.

Reviewed changes

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

File Description
src/CsToml/TomlCodes.cs Introduces BareKeyChars as a SearchValues<byte> set for SIMD/vectorized scans.
src/CsToml/CsTomlReader.cs Switches unquoted key scanning to IndexOfAnyExcept for improved performance and reduced branching.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/CsToml/CsTomlReader.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@prozolic
prozolic merged commit 6a92261 into main Jul 11, 2026
1 check passed
@prozolic
prozolic deleted the readbarekey branch July 11, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants