msbuild icon indicating copy to clipboard operation
msbuild copied to clipboard

Use FrozenSet to identify Modifiers

Open rainersigwald opened this issue 4 months ago • 0 comments

This should be as high-performance as the current custom code and easier to understand and maintain.

I tried a few options and this is slightly better than the existing code. This method is called 11729495 times in an OrchardCore build.

Method Job Runtime Mean Error StdDev Ratio RatioSD
RegularSet .NET 9.0 .NET 9.0 0.5534 ns 0.0097 ns 0.0086 ns 1.19 0.03
Old .NET 9.0 .NET 9.0 0.4639 ns 0.0087 ns 0.0086 ns 1.00 0.03
Frozen .NET 9.0 .NET 9.0 0.4524 ns 0.0046 ns 0.0043 ns 0.98 0.02
Frozen_CaseSensitive_Then_Fallback .NET 9.0 .NET 9.0 0.5143 ns 0.0098 ns 0.0105 ns 1.11 0.03
RegularSet .NET Framework 4.7.2 .NET Framework 4.7.2 0.5058 ns 0.0081 ns 0.0063 ns 1.03 0.02
Old .NET Framework 4.7.2 .NET Framework 4.7.2 0.4910 ns 0.0085 ns 0.0071 ns 1.00 0.02
Frozen .NET Framework 4.7.2 .NET Framework 4.7.2 0.4695 ns 0.0076 ns 0.0071 ns 0.96 0.02
Frozen_CaseSensitive_Then_Fallback .NET Framework 4.7.2 .NET Framework 4.7.2 0.4800 ns 0.0038 ns 0.0031 ns 0.98 0.01
Benchmark code

isitemspecmodifier_inputs.txt was collected by changing IsItemSpecModifier to dump every string passed to it to a text file and running an OrchardCore (single-proc) build.

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

namespace MyBenchmarks;

[SimpleJob(runtimeMoniker: RuntimeMoniker.Net90)]
[SimpleJob(runtimeMoniker: RuntimeMoniker.Net472)]
public class Benchmarks
{
    public static readonly string[] _fileLines;

    static Benchmarks()
    {
        _fileLines = File.ReadAllLines(@"S:\work\bdn_IsItemSpecModifier\isitemspecmodifier_inputs.txt");
    }

    internal const string FullPath = "FullPath";
    internal const string RootDir = "RootDir";
    internal const string Filename = "Filename";
    internal const string Extension = "Extension";
    internal const string RelativeDir = "RelativeDir";
    internal const string Directory = "Directory";
    internal const string RecursiveDir = "RecursiveDir";
    internal const string Identity = "Identity";
    internal const string ModifiedTime = "ModifiedTime";
    internal const string CreatedTime = "CreatedTime";
    internal const string AccessedTime = "AccessedTime";
    internal const string DefiningProjectFullPath = "DefiningProjectFullPath";
    internal const string DefiningProjectDirectory = "DefiningProjectDirectory";
    internal const string DefiningProjectName = "DefiningProjectName";
    internal const string DefiningProjectExtension = "DefiningProjectExtension";

    // These are all the well-known attributes.
    internal static readonly string[] All =
        {
                    FullPath,
                    RootDir,
                    Filename,
                    Extension,
                    RelativeDir,
                    Directory,
                    RecursiveDir,    // <-- Not derivable.
                    Identity,
                    ModifiedTime,
                    CreatedTime,
                    AccessedTime,
                    DefiningProjectFullPath,
                    DefiningProjectDirectory,
                    DefiningProjectName,
                    DefiningProjectExtension
                };

    private static readonly HashSet<string> s_tableOfItemSpecModifiers = new HashSet<string>(All, StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Indicates if the given name is reserved for an item-spec modifier.
    /// </summary>
    internal static bool IsItemSpecModifier(string name)
    {
        if (name == null)
        {
            return false;
        }


        /*
         * What follows requires some explanation.
         *
         * This function is called many times and slowness here will be amplified
         * in critical performance scenarios.
         *
         * The following switch statement attempts to identify item spec modifiers that
         * have the exact case that our constants in ItemSpecModifiers have. This is the
         * 99% case.
         *
         * Further, the switch statement can identify certain cases in which there is
         * definitely no chance that 'name' is an item spec modifier. For example, a
         * 7 letter 'name' that doesn't start with 'r' or 'R' can't be RootDir and
         * therefore is not an item spec modifier.
         *
         */
        switch (name.Length)
        {
            case 7: // RootDir
                switch (name[0])
                {
                    default:
                        return false;
                    case 'R': // RootDir
                        if (name == RootDir)
                        {
                            return true;
                        }
                        break;
                    case 'r':
                        break;
                }
                break;
            case 8: // FullPath, Filename, Identity

                switch (name[0])
                {
                    default:
                        return false;
                    case 'F': // Filename, FullPath
                        if (name == FullPath)
                        {
                            return true;
                        }
                        if (name == Filename)
                        {
                            return true;
                        }
                        break;
                    case 'f':
                        break;
                    case 'I': // Identity
                        if (name == Identity)
                        {
                            return true;
                        }
                        break;
                    case 'i':
                        break;
                }
                break;
            case 9: // Extension, Directory
                switch (name[0])
                {
                    default:
                        return false;
                    case 'D': // Directory
                        if (name == Directory)
                        {
                            return true;
                        }
                        break;
                    case 'd':
                        break;
                    case 'E': // Extension
                        if (name == Extension)
                        {
                            return true;
                        }
                        break;
                    case 'e':
                        break;
                }
                break;
            case 11: // RelativeDir, CreatedTime
                switch (name[0])
                {
                    default:
                        return false;
                    case 'C': // CreatedTime
                        if (name == CreatedTime)
                        {
                            return true;
                        }
                        break;
                    case 'c':
                        break;
                    case 'R': // RelativeDir
                        if (name == RelativeDir)
                        {
                            return true;
                        }
                        break;
                    case 'r':
                        break;
                }
                break;
            case 12: // RecursiveDir, ModifiedTime, AccessedTime

                switch (name[0])
                {
                    default:
                        return false;
                    case 'A': // AccessedTime
                        if (name == AccessedTime)
                        {
                            return true;
                        }
                        break;
                    case 'a':
                        break;
                    case 'M': // ModifiedTime
                        if (name == ModifiedTime)
                        {
                            return true;
                        }
                        break;
                    case 'm':
                        break;
                    case 'R': // RecursiveDir
                        if (name == RecursiveDir)
                        {
                            return true;
                        }
                        break;
                    case 'r':
                        break;
                }
                break;
            case 19:
            case 23:
            case 24:
                return IsDefiningProjectModifier(name);
            default:
                // Not the right length for a match.
                return false;
        }

        // Could still be a case-insensitive match.
        bool result = s_tableOfItemSpecModifiers.Contains(name);

        return result;
    }

    /// <summary>
    /// Indicates if the given name is reserved for one of the specific subset of itemspec
    /// modifiers to do with the defining project of the item.
    /// </summary>
    internal static bool IsDefiningProjectModifier(string name)
    {
        switch (name.Length)
        {
            case 19: // DefiningProjectName
                if (name == DefiningProjectName)
                {
                    return true;
                }
                break;
            case 23: // DefiningProjectFullPath
                if (name == DefiningProjectFullPath)
                {
                    return true;
                }
                break;
            case 24: // DefiningProjectDirectory, DefiningProjectExtension

                switch (name[15])
                {
                    default:
                        return false;
                    case 'D': // DefiningProjectDirectory
                        if (name == DefiningProjectDirectory)
                        {
                            return true;
                        }
                        break;
                    case 'd':
                        break;
                    case 'E': // DefiningProjectExtension
                        if (name == DefiningProjectExtension)
                        {
                            return true;
                        }
                        break;
                    case 'e':
                        break;
                }
                break;
            default:
                return false;
        }

        // Could still be a case-insensitive match.
        bool result = s_tableOfItemSpecModifiers.Contains(name);

        return result;
    }

    private static FrozenSet<string> s_froyo = s_tableOfItemSpecModifiers.ToFrozenSet();

    private static FrozenSet<string> s_ordinal = All.ToFrozenSet(StringComparer.Ordinal);


    internal static bool Frozen_IsItemSpecModifier(string s) => s_froyo.Contains(s);
    internal static bool Frozen_CaseSensitive_Then_FallbackIsItemSpecModifier(string s)
    {
        return s_ordinal.Contains(s) || s_froyo.Contains(s);
    }

    [Benchmark(OperationsPerInvoke = 11_729_495)]
    public bool RegularSet()
    {
        bool x = false;
        foreach (string s in _fileLines)
        {
            x = x || s_tableOfItemSpecModifiers.Contains(s);
        }

        return x;
    }

    [Benchmark(Baseline = true, OperationsPerInvoke = 11_729_495)]
    public bool Old()
    {
        bool x = false;
        foreach (string s in _fileLines)
        {
            x = x || IsItemSpecModifier(s);
        }

        return x;
    }

    [Benchmark(OperationsPerInvoke = 11_729_495)]
    public bool Frozen()
    {
        bool x = false;
        foreach (string s in _fileLines)
        {
            x = x || Frozen_IsItemSpecModifier(s);
        }

        return x;
    }

    [Benchmark(OperationsPerInvoke = 11_729_495)]
    public bool Frozen_CaseSensitive_Then_Fallback()
    {
        bool x = false;
        foreach (string s in _fileLines)
        {
            x = x || Frozen_CaseSensitive_Then_FallbackIsItemSpecModifier(s);
        }

        return x;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        System.Console.WriteLine(Benchmarks._fileLines.Length);
        var summary = BenchmarkRunner.Run<Benchmarks>();
    }
}

rainersigwald avatar Jun 11 '25 21:06 rainersigwald