Skip to content

Commit

Permalink
Merge pull request #1 from chrisg32/ModernPerformance
Browse files Browse the repository at this point in the history
NaturalComparer performance improvements
  • Loading branch information
chrisg32 authored May 4, 2022
2 parents ba60d54 + 6731e12 commit c919836
Show file tree
Hide file tree
Showing 15 changed files with 558 additions and 69 deletions.
17 changes: 17 additions & 0 deletions Commons.Benchmark/BenchmarkHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections.ObjectModel;
using System.Reflection;
using Xunit;

namespace Commons.Benchmark;

internal static class BenchmarkHelper
{
public static List<object?[]> GetInlineData<TType>(string methodName)
{
var type = typeof(TType);
var member = type.GetMethod(methodName);
if (member == null) throw new Exception($"Could not find a method named '{methodName}' on type '{type}'.");
return member.CustomAttributes.Where(a => a.AttributeType == typeof(InlineDataAttribute))
.Select(a => (a.ConstructorArguments[0].Value as ReadOnlyCollection<CustomAttributeTypedArgument>)?.Select(v => v.Value).ToArray()).ToList()!;
}
}
20 changes: 20 additions & 0 deletions Commons.Benchmark/Commons.Benchmark.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Commons.Test\Commons.Test.csproj" />
<ProjectReference Include="..\Commons\Commons.csproj" />
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions Commons.Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using BenchmarkDotNet.Running;
using Commons.Benchmark.Util;

BenchmarkRunner.Run<NaturalComparerLargeSimilarStringBenchmarks>();
49 changes: 49 additions & 0 deletions Commons.Benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
### NaturalComparer

#### Benchmark

| Method | Mean | Error | StdDev | Allocated |
|--------------------------------- |-----------:|---------:|---------:|----------:|
| Compare | 222.8 ns | 4.30 ns | 5.12 ns | 360 B |
| CompareIgnoreCase | 350.6 ns | 7.04 ns | 10.32 ns | 536 B |
| CompareIgnoreWhiteSpace | 3,831.2 ns | 46.87 ns | 46.04 ns | 3,464 B |
| CompareIgnoreCaseWhiteSpace | 3,982.3 ns | 37.84 ns | 31.59 ns | 3,640 B |
| Compare_Span | 101.3 ns | 1.82 ns | 1.70 ns | - |
| CompareIgnoreCase_Span | 137.6 ns | 2.49 ns | 2.33 ns | - |
| CompareIgnoreWhiteSpace_Span | 1,632.6 ns | 12.74 ns | 9.95 ns | 376 B |
| CompareIgnoreCaseWhiteSpace_Span | 1,756.9 ns | 29.59 ns | 27.68 ns | 376 B |

#### Large Random String

| Method | Mean | Error | StdDev | Allocated |
|--------------------------------- |-------------:|-----------:|-----------:|----------:|
| Compare | 541.04 ns | 5.690 ns | 5.322 ns | 2,360 B |
| CompareIgnoreCase | 1,917.42 ns | 35.455 ns | 34.822 ns | 4,720 B |
| CompareIgnoreWhiteSpace | 9,676.14 ns | 190.178 ns | 177.892 ns | 7,328 B |
| CompareIgnoreCaseWhiteSpace | 10,953.94 ns | 147.037 ns | 137.539 ns | 10,816 B |
| Compare_Span | 32.67 ns | 0.689 ns | 1.052 ns | - |
| CompareIgnoreCase_Span | 24.55 ns | 0.416 ns | 0.369 ns | - |
| CompareIgnoreWhiteSpace_Span | 3,440.01 ns | 66.125 ns | 73.498 ns | 4,704 B |
| CompareIgnoreCaseWhiteSpace_Span | 3,797.10 ns | 49.217 ns | 41.098 ns | 4,688 B |

#### Large Similar String

| Method | Mean | Error | StdDev | Allocated |
|--------------------------------- |---------:|----------:|----------:|----------:|
| Compare | 4.988 us | 0.0638 us | 0.0597 us | 2,736 B |
| CompareIgnoreCase | 5.421 us | 0.0992 us | 0.0928 us | 2,736 B |
| CompareIgnoreWhiteSpace | 8.286 us | 0.1003 us | 0.0938 us | 4,712 B |
| CompareIgnoreCaseWhiteSpace | 8.806 us | 0.1670 us | 0.1787 us | 4,712 B |
| Compare_Span | 2.336 us | 0.0234 us | 0.0183 us | - |
| CompareIgnoreCase_Span | 2.905 us | 0.0336 us | 0.0314 us | - |
| CompareIgnoreWhiteSpace_Span | 3.189 us | 0.0609 us | 0.0570 us | 928 B |
| CompareIgnoreCaseWhiteSpace_Span | 3.757 us | 0.0733 us | 0.0612 us | 928 B |

##### Test Machine
```
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1645 (21H2)
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.202
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
```
61 changes: 61 additions & 0 deletions Commons.Benchmark/Util/NaturalComparerBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using BenchmarkDotNet.Attributes;
using CG.Commons.Util;
#pragma warning disable CS0612

namespace Commons.Benchmark.Util;

[MemoryDiagnoser(false)]
public class NaturalComparerBenchmarks
{
private NaturalComparerObsolete _comparerObsolete = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCase = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreWhitespace = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCaseWhitespace = null!;
private NaturalComparer _comparer = null!;
private NaturalComparer _comparerIgnoreCase = null!;
private NaturalComparer _comparerIgnoreWhitespace = null!;
private NaturalComparer _comparerIgnoreCaseWhitespace = null!;

[GlobalSetup]
public void Setup()
{
_comparerObsolete = new NaturalComparerObsolete();
_comparerObsoleteIgnoreCase = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase);
_comparerObsoleteIgnoreWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerObsoleteIgnoreCaseWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);

_comparer = new NaturalComparer();
_comparerIgnoreCase = new NaturalComparer(NaturalComparerOptions.IgnoreCase);
_comparerIgnoreWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerIgnoreCaseWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);
}

private const string Left = " ThisIsA StringWithANumber00201.3 ";
private const string Right = " ThisIsA StringWithANumber00100.6 ";

[Benchmark]
public void Compare() => _ = _comparerObsolete.Compare(Left , Right);

[Benchmark]
public void CompareIgnoreCase() => _ = _comparerObsoleteIgnoreCase.Compare(Left, Right);

[Benchmark]
public void CompareIgnoreWhiteSpace() => _ = _comparerObsoleteIgnoreWhitespace.Compare(Left, Right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace() => _ = _comparerObsoleteIgnoreCaseWhitespace.Compare(Left, Right);



[Benchmark]
public void Compare_Span() => _ = _comparer.Compare(Left , Right);

[Benchmark]
public void CompareIgnoreCase_Span() => _ = _comparerIgnoreCase.Compare(Left, Right);

[Benchmark]
public void CompareIgnoreWhiteSpace_Span() => _ = _comparerIgnoreWhitespace.Compare(Left, Right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace_Span() => _ = _comparerIgnoreCaseWhitespace.Compare(Left, Right);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using BenchmarkDotNet.Attributes;
using CG.Commons.Util;
#pragma warning disable CS0612

namespace Commons.Benchmark.Util;

[MemoryDiagnoser(false)]
public class NaturalComparerLargeRandomStringBenchmarks
{
private NaturalComparerObsolete _comparerObsolete = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCase = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreWhitespace = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCaseWhitespace = null!;
private NaturalComparer _comparer = null!;
private NaturalComparer _comparerIgnoreCase = null!;
private NaturalComparer _comparerIgnoreWhitespace = null!;
private NaturalComparer _comparerIgnoreCaseWhitespace = null!;

private string _left = null!;
private string _right = null!;

[GlobalSetup]
public void Setup()
{
_comparerObsolete = new NaturalComparerObsolete();
_comparerObsoleteIgnoreCase = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase);
_comparerObsoleteIgnoreWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerObsoleteIgnoreCaseWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);

_comparer = new NaturalComparer();
_comparerIgnoreCase = new NaturalComparer(NaturalComparerOptions.IgnoreCase);
_comparerIgnoreWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerIgnoreCaseWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);

_left = GenerateString(600);
_right = GenerateString(555);
}

private static string GenerateString(int i)
{
var random = new Random();
var array = new char[i--];
for (; i >= 0; i--)
{
array[i] = (char)random.Next(32, 122);
}
return new string(array);
}


[Benchmark]
public void Compare() => _ = _comparerObsolete.Compare(_left , _right);

[Benchmark]
public void CompareIgnoreCase() => _ = _comparerObsoleteIgnoreCase.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreWhiteSpace() => _ = _comparerObsoleteIgnoreWhitespace.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace() => _ = _comparerObsoleteIgnoreCaseWhitespace.Compare(_left, _right);



[Benchmark]
public void Compare_Span() => _ = _comparer.Compare(_left , _right);

[Benchmark]
public void CompareIgnoreCase_Span() => _ = _comparerIgnoreCase.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreWhiteSpace_Span() => _ = _comparerIgnoreWhitespace.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace_Span() => _ = _comparerIgnoreCaseWhitespace.Compare(_left, _right);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using BenchmarkDotNet.Attributes;
using CG.Commons.Util;
#pragma warning disable CS0612

namespace Commons.Benchmark.Util;

[MemoryDiagnoser(false)]
public class NaturalComparerLargeSimilarStringBenchmarks
{
private NaturalComparerObsolete _comparerObsolete = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCase = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreWhitespace = null!;
private NaturalComparerObsolete _comparerObsoleteIgnoreCaseWhitespace = null!;
private NaturalComparer _comparer = null!;
private NaturalComparer _comparerIgnoreCase = null!;
private NaturalComparer _comparerIgnoreWhitespace = null!;
private NaturalComparer _comparerIgnoreCaseWhitespace = null!;

private string _left = null!;
private string _right = null!;

[GlobalSetup]
public void Setup()
{
_comparerObsolete = new NaturalComparerObsolete();
_comparerObsoleteIgnoreCase = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase);
_comparerObsoleteIgnoreWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerObsoleteIgnoreCaseWhitespace = new NaturalComparerObsolete(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);

_comparer = new NaturalComparer();
_comparerIgnoreCase = new NaturalComparer(NaturalComparerOptions.IgnoreCase);
_comparerIgnoreWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreWhiteSpace);
_comparerIgnoreCaseWhitespace = new NaturalComparer(NaturalComparerOptions.IgnoreCase | NaturalComparerOptions.IgnoreWhiteSpace);

_left = GenerateString(20, "abcdefg01.01");
_right = GenerateString(20, "abcdefg01.02");
}

public static string GenerateString(int times, string seed)
{
var decimals = seed.Count(c => c == '.');
var array = new char[times * (seed.Length - decimals) + decimals];
var i = 0;
foreach (var c in seed)
{
if (c == '.')
{
array[i++] = c;
}
else
{
for (var j = 0; j < times; j++, i++)
{
array[i] = c;
}
}
}
return new string(array);
}


[Benchmark]
public void Compare() => _ = _comparerObsolete.Compare(_left , _right);

[Benchmark]
public void CompareIgnoreCase() => _ = _comparerObsoleteIgnoreCase.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreWhiteSpace() => _ = _comparerObsoleteIgnoreWhitespace.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace() => _ = _comparerObsoleteIgnoreCaseWhitespace.Compare(_left, _right);



[Benchmark]
public void Compare_Span() => _ = _comparer.Compare(_left , _right);

[Benchmark]
public void CompareIgnoreCase_Span() => _ = _comparerIgnoreCase.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreWhiteSpace_Span() => _ = _comparerIgnoreWhitespace.Compare(_left, _right);

[Benchmark]
public void CompareIgnoreCaseWhiteSpace_Span() => _ = _comparerIgnoreCaseWhitespace.Compare(_left, _right);
}
10 changes: 3 additions & 7 deletions Commons.Test/Commons.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>

Expand All @@ -11,13 +11,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
15 changes: 10 additions & 5 deletions Commons.Test/Util/NaturalComparerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using CG.Commons.Util;
using Xunit;

Expand Down Expand Up @@ -80,28 +81,32 @@ public enum ComparerEquality
[InlineData("aa", " a\ta", ComparerEquality.Equal, NaturalComparerOptions.IgnoreWhiteSpace)]
//capitalization order
[InlineData("added4", "Added11", ComparerEquality.LessThan)]
[InlineData("added4", "Added11", ComparerEquality.GreaterThan, NaturalComparerOptions.LowercaseFirst)]
//double decimals
[InlineData("12.4.1", "12.4.1", ComparerEquality.Equal)]
[InlineData("12.41", "12.4.1", ComparerEquality.GreaterThan)]
public void TestCompare(string left, string right, ComparerEquality expectedResult, NaturalComparerOptions options = NaturalComparerOptions.None)
{
var comparerOld = new NaturalComparerObsolete(options);
DoTest(left, right, expectedResult, comparerOld, nameof(NaturalComparerObsolete));

var comparer = new NaturalComparer(options);
DoTest(left, right, expectedResult, comparer);
DoTest(left, right, expectedResult, comparer, nameof(NaturalComparer));
}

private static void DoTest(string left, string right, ComparerEquality expectedResult, NaturalComparer comparer)
private static void DoTest(string left, string right, ComparerEquality expectedResult, IComparer<string> comparer, string note)
{
var result = comparer.Compare(left, right);
switch (expectedResult)
{
case ComparerEquality.LessThan:
Assert.True(result <= (int)expectedResult, $"Result: {result} Expected Result: {expectedResult}({(int)expectedResult})");
Assert.True(result <= (int)expectedResult, $"Result: {result} Expected: {expectedResult}({(int)expectedResult}) - {note}");
break;
case ComparerEquality.Equal:
Assert.True(result == (int)expectedResult, $"Result: {result} Expected Result: {expectedResult}({(int)expectedResult})");
Assert.True(result == (int)expectedResult, $"Result: {result} Expected: {expectedResult}({(int)expectedResult}) - {note}");
break;
case ComparerEquality.GreaterThan:
Assert.True(result >= (int)expectedResult, $"Result: {result} Expected Result: {expectedResult}({(int)expectedResult})");
Assert.True(result >= (int)expectedResult, $"Result: {result} Expected: {expectedResult}({(int)expectedResult}) - {note}");
break;
default:
throw new ArgumentOutOfRangeException(nameof(expectedResult), expectedResult, null);
Expand Down
Loading

0 comments on commit c919836

Please sign in to comment.