aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

`IsCharWhitespace` for HTML5 spec without char[].IndexOf

Open gfoidl opened this issue 1 year ago • 2 comments

In MVC's tag-helpers for the check IsCharWhitespace a static readonly char[] with the allowed chars was used and then the IndexOf(ch) to see if the given char is in that set or not. For the five allowed chars this operation is quite costly, and the call to IndexOf can't be inlined as it's too big.

As kind of direct lookup we can construct a bit-mask from the allowed chars, then check if the given char is in that bit-mask. Results in speedup ~10x (for this isolated operation in the benchmark). Moreover the code becomes very tiny, so it can be inlined -- though the JIT needs some help with that decision.

Benchmark code
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;

Bench bench = new();
Console.WriteLine(bench.Default());
Console.WriteLine(bench.BitMask());
Console.WriteLine(bench.BitMaskInlined());

#if !DEBUG
BenchmarkDotNet.Running.BenchmarkRunner.Run<Bench>();
#endif

[DisassemblyDiagnoser]
public class Bench
{
    private static readonly char[] ValidAttributeWhitespaceChars =
        new[] { '\t', '\n', '\u000C', '\r', ' ' };

    [Benchmark(Baseline = true, OperationsPerInvoke = char.MaxValue - char.MinValue)]
    public int Default()
    {
        int count = 0;

        for (int i = char.MinValue; i <= char.MaxValue; ++i)
        {
            if (IsCharWhitespace((char)i))
            {
                count++;
            }
        }

        return count;
    }

    [Benchmark(OperationsPerInvoke = char.MaxValue - char.MinValue)]
    public int BitMask()
    {
        int count = 0;

        for (int i = char.MinValue; i <= char.MaxValue; ++i)
        {
            if (IsCharWhitespaceBitMask((char)i))
            {
                count++;
            }
        }

        return count;
    }

    [Benchmark(OperationsPerInvoke = char.MaxValue - char.MinValue)]
    public int BitMaskInlined()
    {
        int count = 0;

        for (int i = char.MinValue; i <= char.MaxValue; ++i)
        {
            if (IsCharWhitespaceBitMaskInlined((char)i))
            {
                count++;
            }
        }

        return count;
    }

    private static bool IsCharWhitespace(char ch)
    {
        return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
    }

    private static bool IsCharWhitespaceBitMask(char ch)
    {
        if (ch > 32) return false;

        // Valid whitespace characters defined by the HTML5 spec.
        const long BitMask =
            (1L << '\t')    // 9
          | (1L << '\n')    // 10
          | (1L << '\f')    // 12
          | (1L << '\r')    // 13
          | (1L << ' ');    // 32

        return (BitMask & (1L << ch)) != 0;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsCharWhitespaceBitMaskInlined(char ch)
    {
        if (ch > 32) return false;

        // Valid whitespace characters defined by the HTML5 spec.
        const long BitMask =
            (1L << '\t')    // 9
          | (1L << '\n')    // 10
          | (1L << '\f')    // 12
          | (1L << '\r')    // 13
          | (1L << ' ');    // 32

        return (BitMask & (1L << ch)) != 0;
    }
}
|         Method |      Mean |     Error |    StdDev | Ratio | Code Size |
|--------------- |----------:|----------:|----------:|------:|----------:|
|        Default | 6.0844 ns | 0.1193 ns | 0.1374 ns |  1.00 |     107 B |
|        BitMask | 2.1983 ns | 0.0435 ns | 0.0714 ns |  0.37 |      85 B |
| BitMaskInlined | 0.6276 ns | 0.0125 ns | 0.0162 ns |  0.10 |      51 B |

(.NET SDK=7.0.100-rc.2.22426.5)

Method BitMaskInlined is used in this PR.

gfoidl avatar Sep 15 '22 12:09 gfoidl

Thanks for your PR, @gfoidl. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

msftbot[bot] avatar Sep 15 '22 12:09 msftbot[bot]

Thanks for sending this PR, @gfoidl. @TanayParikh can you please review this? Thanks!

mkArtakMSFT avatar Sep 15 '22 16:09 mkArtakMSFT