msbuild
msbuild copied to clipboard
Use FrozenSet to identify Modifiers
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>();
}
}