Skip to content

Commit 85c2003

Browse files
steve7411Stephenchalcolith
authored
Features/emumeration optimizations (#35)
These changes reduce an O(n^2) enumeration for the common case of string inputs to O(n), amongst a few other things. I changed MatchState to create a Slice in any case where the input isn't already an IList, and Slice is now a thin wrapper around adapters that implement IList. I tried not to change publicly exposed API much, but MatchState's Input property is read only and I didn't keep Slice's copy on write since it isn't currently used anywhere. Co-authored-by: Stephen <stephen@7thg.com> Co-authored-by: Gordon Tisher <gordon@balafon.net>
1 parent d70eb36 commit 85c2003

File tree

17 files changed

+517
-401
lines changed

17 files changed

+517
-401
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,4 @@ Generated_Code #added for RIA/Silverlight projects
110110
_UpgradeReport_Files/
111111
Backup*/
112112
UpgradeLog*.XML
113+
UpgradeLog*.htm

IronMeta.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
2222
README.md = README.md
2323
EndProjectSection
2424
EndProject
25+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IronMeta.Benchmarks", "Source\IronMeta.Benchmarks\IronMeta.Benchmarks.csproj", "{33D8CE3F-B6B9-4545-B16D-2D7AE83199BF}"
26+
EndProject
2527
Global
2628
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2729
Debug|Any CPU = Debug|Any CPU
@@ -46,6 +48,10 @@ Global
4648
{9FFE9F3E-712A-4076-9F70-A1A03D3BB820}.Release|Any CPU.Build.0 = Release|Any CPU
4749
{1522FF5D-1356-45F4-BED6-D653DEE0AFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
4850
{1522FF5D-1356-45F4-BED6-D653DEE0AFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
51+
{33D8CE3F-B6B9-4545-B16D-2D7AE83199BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52+
{33D8CE3F-B6B9-4545-B16D-2D7AE83199BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
53+
{33D8CE3F-B6B9-4545-B16D-2D7AE83199BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
54+
{33D8CE3F-B6B9-4545-B16D-2D7AE83199BF}.Release|Any CPU.Build.0 = Release|Any CPU
4955
EndGlobalSection
5056
GlobalSection(SolutionProperties) = preSolution
5157
HideSolutionNode = FALSE

Samples/Calc/Calc.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFrameworks>net48;netcoreapp3.1;net5.0</TargetFrameworks>
6+
<LangVersion>latest</LangVersion>
67
<RootNamespace>IronMeta.Samples.Calc</RootNamespace>
78
<Version>4.4.6</Version>
89
<Authors>Gordon Tisher</Authors>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
using BenchmarkDotNet.Jobs;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace IronMeta.Benchmarks
8+
{
9+
10+
[SimpleJob(RuntimeMoniker.Net48, baseline: true)]
11+
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
12+
[SimpleJob(RuntimeMoniker.Net50)]
13+
[RPlotExporter]
14+
public class Benchmarks
15+
{
16+
private static List<char> inputList;
17+
private static char[] inputArray;
18+
private static string inputString;
19+
20+
static IEnumerable<int> RandomInts(int count, int min, int max)
21+
{
22+
// We need this to generate the same numbers every time, so hard coding arbitrary seed
23+
var rand = new Random(289759387);
24+
for (int i = 0; i < count; i++)
25+
yield return rand.Next(min, max);
26+
}
27+
28+
static string GenerateInput(int count)
29+
{
30+
var rand = new Random(289759387);
31+
var sb = new System.Text.StringBuilder(count * 5);
32+
var ops = new[] { '+', '-', '*' };
33+
foreach (var n in RandomInts(count, 0, 10_000))
34+
{
35+
sb.Append(n);
36+
sb.Append(ops[rand.Next(0, ops.Length)]);
37+
}
38+
sb.Append('0');
39+
return sb.ToString();
40+
}
41+
42+
[GlobalSetup]
43+
public void GlobalSetup()
44+
{
45+
inputString = GenerateInput(1_250);
46+
inputList = inputString.ToList();
47+
inputArray = inputString.ToArray();
48+
}
49+
50+
[Benchmark]
51+
public void CalcParserBenchmark()
52+
{
53+
var parser = new IronMeta.Samples.Calc.Calc();
54+
var match = parser.GetMatch(inputString, parser.Expression);
55+
if (match.NextIndex != inputString.Length)
56+
Console.Error.WriteLine("INPUT NOT FULLY PARSED");
57+
try
58+
{
59+
var r = match.Result;
60+
} catch (Exception)
61+
{
62+
Console.Error.WriteLine($"Error: {match.Error}");
63+
}
64+
}
65+
}
66+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net48;netcoreapp3.1;net5.0</TargetFrameworks>
5+
<OutputType>Exe</OutputType>
6+
<LangVersion>latest</LangVersion>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\Samples\Calc\Calc.csproj" />
15+
</ItemGroup>
16+
17+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using BenchmarkDotNet.Running;
2+
using System;
3+
4+
namespace IronMeta.Benchmarks
5+
{
6+
class Program
7+
{
8+
static void Main(string[] args)
9+
{
10+
//ManualRun();
11+
var summary = BenchmarkRunner.Run<Benchmarks>();
12+
}
13+
14+
static void ManualRun()
15+
{
16+
var benchmark = new Benchmarks();
17+
benchmark.GlobalSetup();
18+
benchmark.CalcParserBenchmark();
19+
}
20+
}
21+
}

Source/IronMeta.Library/IronMeta.Library.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net48;netstandard2.1;net5.0</TargetFrameworks>
4+
<TargetFrameworks>net48;netcoreapp3.1;net5.0</TargetFrameworks>
5+
<LangVersion>latest</LangVersion>
56
<RootNamespace>IronMeta</RootNamespace>
67
<SignAssembly>true</SignAssembly>
78
<AssemblyOriginatorKeyFile>IronMeta.snk</AssemblyOriginatorKeyFile>
@@ -33,7 +34,7 @@
3334
<PackagePath>tools\net48</PackagePath>
3435
</Content>
3536

36-
<Content Include="..\IronMeta.App\bin\Release\net5.0\IronMeta.App.exe">
37+
<Content Include="..\IronMeta.App\bin\Release\net5.0\IronMeta.App.exe" Condition=" '$(OS)' == 'Windows_NT' ">
3738
<PackagePath>tools\net5.0</PackagePath>
3839
</Content>
3940
<Content Include="..\IronMeta.App\bin\Release\net5.0\IronMeta.App.dll">

Source/IronMeta.Library/Matcher/MatchItem.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66

77
using IronMeta.Utils;
8+
using IronMeta.Utils.Slices;
89

910
namespace IronMeta.Matcher
1011
{
@@ -67,13 +68,10 @@ public IEnumerable<TInput> Inputs
6768
{
6869
if (input_slice == null)
6970
{
70-
if (input_start >= 0 && input_next >= 0)
71+
if (input_start >= 0 && input_next > input_start)
7172
{
72-
TInput[] input_array = input_enumerable as TInput[];
73-
if (input_array != null)
74-
input_slice = new ArraySegment<TInput>(input_array, input_start, input_next - input_start);
75-
else
76-
input_slice = new Slice<TInput>(input_enumerable, input_start, input_next - input_start);
73+
int count = input_next - input_start;
74+
input_slice = new Slice<TInput>(input_enumerable, input_start, count);
7775
}
7876
else
7977
{

Source/IronMeta.Library/Matcher/MatchResult.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// IronMeta Copyright © Gordon Tisher
22

3+
using System;
34
using System.Collections.Generic;
45
using System.Linq;
56

@@ -17,6 +18,7 @@ public class MatchResult<TInput, TResult>
1718
int start = -1;
1819
int next = -1;
1920
IEnumerable<TResult> result;
21+
Func<string> error_func;
2022
string error;
2123
int errorIndex;
2224

@@ -31,15 +33,16 @@ internal MatchResult()
3133
/// Constructor.
3234
/// </summary>
3335
internal MatchResult(Matcher<TInput, TResult> matcher, MatchState<TInput, TResult> memo,
34-
bool success, int start, int next, IEnumerable<TResult> result, string error, int errorIndex)
36+
bool success, int start, int next, IEnumerable<TResult> result, Func<string> error_func, int errorIndex)
3537
{
3638
this.matcher = matcher;
3739
this.state = memo;
3840
this.success = success;
3941
this.start = start;
4042
this.next = next;
4143
this.result = result;
42-
this.error = error;
44+
this.error_func = error_func;
45+
//this.error = error_func();
4346
this.errorIndex = errorIndex;
4447
}
4548

@@ -82,7 +85,13 @@ internal MatchResult(Matcher<TInput, TResult> matcher, MatchState<TInput, TResul
8285
/// <summary>
8386
/// The error that caused the match to fail, if it failed.
8487
/// </summary>
85-
public string Error { get { return error; } }
88+
public string Error
89+
{
90+
get
91+
{
92+
return error ??= error_func();
93+
}
94+
}
8695

8796
/// <summary>
8897
/// The index in the input stream at which the error occurred.

Source/IronMeta.Library/Matcher/MatchState.cs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Text.RegularExpressions;
88

99
using IronMeta.Utils;
10+
using IronMeta.Utils.Slices;
1011

1112
namespace IronMeta.Matcher
1213
{
@@ -30,26 +31,16 @@ IDictionary<string, Dictionary<int, LRRecord<MatchItem<TInput, TResult>>>> curre
3031
/// <summary>
3132
/// The input stream for the grammar to parse.
3233
/// </summary>
33-
public IEnumerable<TInput> Input
34-
{
35-
get { return InputEnumerable; }
36-
37-
protected set
38-
{
39-
InputEnumerable = value;
40-
InputList = InputEnumerable as IList<TInput>;
41-
InputString = InputEnumerable as string;
42-
}
43-
}
34+
public IEnumerable<TInput> Input => InputList;
4435

4536
/// <summary>
4637
/// The input enumerable for this match.
4738
/// </summary>
48-
public IEnumerable<TInput> InputEnumerable { get; set; }
39+
public IEnumerable<TInput> InputEnumerable => InputList;
4940

5041
/// <summary>
5142
/// The input enumerable for this match (<see cref="InputEnumerable"/>) as an <c>IList&lt;&gt;</c>.
52-
/// Is <c>null</c> if the input enumerable is not an <c>IList&lt;&gt;</c>.
43+
/// Should never be <c>null</c>, because non-lists will be wrapped.
5344
/// </summary>
5445
public IList<TInput> InputList { get; set; }
5546

@@ -149,7 +140,17 @@ public int LastErrorIndex
149140
/// <param name="input">Input to be matched.</param>
150141
public MatchState(IEnumerable<TInput> input)
151142
{
152-
Input = (input is string || input is IList<TInput>) ? input : new Memoizer<TInput>(input);
143+
InputList = input switch
144+
{
145+
IList<TInput> list => list,
146+
_ => new Slice<TInput>(input),
147+
};
148+
InputString = input switch
149+
{
150+
string str => str,
151+
Slice<TInput> slice => slice.GetStringIfCheap(),
152+
_ => null,
153+
};
153154
Results = new Stack<MatchItem<TInput, TResult>>();
154155
ArgResults = new Stack<MatchItem<TInput, TResult>>();
155156
CallStack = new Stack<LRRecord<MatchItem<TInput, TResult>>>();

0 commit comments

Comments
 (0)