diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IsPatternOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IsPatternOperator.cs index ddfa648b887a..150706aa4f2d 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IsPatternOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IsPatternOperator.cs @@ -13,8 +13,9 @@ namespace Microsoft.CodeAnalysis.CSharp internal sealed partial class LocalRewriter { /// - /// Benchmark results (see https://github.com/dotnet/roslyn/pull/84961) show that short patterns can be more - /// efficient when emitted as comparisons. Larger patterns can benefit from switch dispatch. + /// Benchmarks in src/Tools/Benchmarks/IsPatternBenchmarks.cs show that short sparse patterns can be more + /// efficient as comparisons, while the sparse and dense four-value cases show that larger patterns can benefit + /// from general decision-DAG dispatch. /// private const int MaxTestsForInvertedLinearSequence = 3; diff --git a/src/Tools/Benchmarks/IsPatternBenchmarks.cs b/src/Tools/Benchmarks/IsPatternBenchmarks.cs new file mode 100644 index 000000000000..d416defbc88e --- /dev/null +++ b/src/Tools/Benchmarks/IsPatternBenchmarks.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks; + +public enum IsPatternInputPosition +{ + FirstHit, + LastHit, + Miss, +} + +public enum IsPatternPathInput +{ + Empty, + Slash, + Backslash, + Other, +} + +/// +/// The pattern and comparison forms are expected to have equivalent performance when short patterns use linear +/// lowering. This benchmark detects regressions in that equivalence. +/// +/// +[EvaluateOverhead(false)] +public class IsPatternLeadingSlashBenchmarks +{ + private string _path = null!; + + [Params(IsPatternPathInput.Empty, IsPatternPathInput.Slash, IsPatternPathInput.Backslash, IsPatternPathInput.Other)] + public IsPatternPathInput Input { get; set; } + + [GlobalSetup] + public void Setup() + { + _path = Input switch + { + IsPatternPathInput.Empty => "", + IsPatternPathInput.Slash => "/path", + IsPatternPathInput.Backslash => "\\path", + IsPatternPathInput.Other => "path", + _ => throw new InvalidOperationException(), + }; + } + + [Benchmark(Baseline = true)] + public bool Comparisons() => HasLeadingSlashWithComparisons(_path); + + [Benchmark] + public bool Pattern() => HasLeadingSlashWithPattern(_path); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool HasLeadingSlashWithComparisons(ReadOnlySpan path) + { + if (path.Length > 0 && (path[0] == '/' || path[0] == '\\')) + return true; + + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool HasLeadingSlashWithPattern(ReadOnlySpan path) + { + if (path.Length > 0 && path[0] is '/' or '\\') + return true; + + return false; + } +} + +/// +/// The pattern and comparison forms are expected to have equivalent performance when short patterns use linear +/// lowering. This benchmark detects regressions in that equivalence. +/// +/// +[EvaluateOverhead(false)] +public class IsPatternJsonTerminatorBenchmarks : IsPatternByteInputBenchmarks +{ + protected override byte FirstCase => (byte)'.'; + protected override byte LastCase => (byte)'e'; + + [Benchmark(Baseline = true)] + public int Comparisons() + { + var value = Value; + return value != (byte)'.' && value != (byte)'E' && value != (byte)'e' + ? value + 1 + : value - 1; + } + + [Benchmark] + public int Pattern() + { + var value = Value; + return value is not ((byte)'.' or (byte)'E' or (byte)'e') + ? value + 1 + : value - 1; + } +} + +/// +/// Compares explicit linear tests with general decision-DAG dispatch for four sparse values. +/// +/// +[EvaluateOverhead(false)] +public class IsPatternSparseFourValueBenchmarks : IsPatternByteInputBenchmarks +{ + protected override byte FirstCase => (byte)'+'; + protected override byte LastCase => (byte)'e'; + + [Benchmark(Baseline = true)] + public bool Comparisons() => Guard && (Value == (byte)'+' || Value == (byte)'.' || Value == (byte)'E' || Value == (byte)'e'); + + [Benchmark] + public bool Pattern() => Guard && Value is (byte)'+' or (byte)'.' or (byte)'E' or (byte)'e'; +} + +/// +/// Compares explicit linear tests with general decision-DAG dispatch for four dense values. +/// +/// +[EvaluateOverhead(false)] +public class IsPatternDenseFourValueBenchmarks : IsPatternByteInputBenchmarks +{ + protected override byte FirstCase => 1; + protected override byte LastCase => 4; + + [Benchmark(Baseline = true)] + public bool Comparisons() => Guard && (Value == 1 || Value == 2 || Value == 3 || Value == 4); + + [Benchmark] + public bool Pattern() => Guard && Value is 1 or 2 or 3 or 4; +} + +public abstract class IsPatternByteInputBenchmarks +{ + [Params(IsPatternInputPosition.FirstHit, IsPatternInputPosition.LastHit, IsPatternInputPosition.Miss)] + public IsPatternInputPosition Input { get; set; } + + protected bool Guard; + protected byte Value; + + protected abstract byte FirstCase { get; } + protected abstract byte LastCase { get; } + + [GlobalSetup] + public void Setup() + { + Guard = true; + Value = Input switch + { + IsPatternInputPosition.FirstHit => FirstCase, + IsPatternInputPosition.LastHit => LastCase, + IsPatternInputPosition.Miss => byte.MaxValue, + _ => throw new InvalidOperationException(), + }; + } +}