Skip to content

Commit 700c35e

Browse files
Minor perf improvements.
1 parent 23658bd commit 700c35e

3 files changed

Lines changed: 103 additions & 25 deletions

File tree

Benchmarks/EnumParseTests.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using BenchmarkDotNet.Attributes;
22
using FastEnumUtility;
3+
using System.Security;
34

45
namespace Open.Text.Benchmarks;
56

@@ -39,7 +40,7 @@ public bool UseValid
3940
}
4041
}
4142

42-
[Params(false, true)]
43+
[Params(false/*, true*/)]
4344
public bool IgnoreCase
4445
{
4546
get => ignoreCase;
@@ -50,8 +51,8 @@ public bool IgnoreCase
5051
}
5152
}
5253

53-
static readonly string[] ValidValues = new string[] { nameof(Greek.Alpha), nameof(Greek.Epsilon), nameof(Greek.Phi) };
54-
static readonly string[] InvalidValues = new string[] { "Apple", "Orange", "Pineapple" };
54+
static readonly string[] ValidValues = new string[] { nameof(Greek.Alpha), nameof(Greek.Epsilon), nameof(Greek.Phi), nameof(Greek.Beta), nameof(Greek.Gamma) };
55+
static readonly string[] InvalidValues = new string[] { "Apple", "Orange", "Pineapple", "Grapefruit", "Lemon" };
5556

5657
// To avoid branching overhead when benchmarking.
5758
abstract class Tests
@@ -65,6 +66,25 @@ abstract class Tests
6566
public abstract Greek CompiledSwitch();
6667

6768
public abstract Greek CompiledSwitchByLength();
69+
70+
static readonly IDictionary<string, Greek> LookupD
71+
= Enum
72+
.GetValues<Greek>()
73+
.ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.Ordinal);
74+
75+
protected virtual bool Lookup(string value, out Greek e)
76+
=> LookupD.TryGetValue(value, out e);
77+
78+
public Greek DictionaryLookup()
79+
{
80+
Greek e = default;
81+
foreach (string s in ValidValues)
82+
{
83+
if (!Lookup(s, out e))
84+
throw new Exception("Invalid.");
85+
}
86+
return e;
87+
}
6888
}
6989

7090
class ValidTests : Tests
@@ -239,6 +259,14 @@ public override Greek FastEnumParse()
239259
}
240260
return e;
241261
}
262+
263+
static readonly IDictionary<string, Greek> LookupD
264+
= Enum
265+
.GetValues<Greek>()
266+
.ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.OrdinalIgnoreCase);
267+
268+
protected override bool Lookup(string value, out Greek e)
269+
=> LookupD.TryGetValue(value, out e);
242270
}
243271

244272
class InvalidTestsIC : Tests
@@ -281,7 +309,7 @@ public override Greek EnumValueParse()
281309
Greek e = default;
282310
foreach (string s in InvalidValues)
283311
{
284-
if (EnumValue.TryParse(s, true, out e))
312+
if (EnumValue.TryParseIgnoreCase(s, out e))
285313
throw new Exception("Valid.");
286314
}
287315
return e;
@@ -297,6 +325,14 @@ public override Greek FastEnumParse()
297325
}
298326
return e;
299327
}
328+
329+
static readonly IDictionary<string, Greek> LookupD
330+
= Enum
331+
.GetValues<Greek>()
332+
.ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.OrdinalIgnoreCase);
333+
334+
protected override bool Lookup(string value, out Greek e)
335+
=> LookupD.TryGetValue(value, out e);
300336
}
301337

302338
private static bool TryParseBySwitch(string value, out Greek e)
@@ -572,8 +608,12 @@ private static bool TryParseByLengthSwitchCaseIgnored(string value, out Greek e)
572608
public Greek CompiledSwitchByLengths() => _tests.CompiledSwitchByLength();
573609

574610
[Benchmark]
611+
// Uses an expression tree when case sensitive.
575612
public Greek EnumValueParse() => _tests.EnumValueParse();
576613

577614
[Benchmark]
578615
public Greek FastEnumParse() => _tests.FastEnumParse();
616+
617+
[Benchmark]
618+
public Greek DictionaryLookup() => _tests.DictionaryLookup();
579619
}

Source/EnumValue.cs

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Microsoft.Extensions.Primitives;
1+
// Ignore Spelling: Deconstruct
2+
3+
using Microsoft.Extensions.Primitives;
24
using System;
35
using System.Collections.Concurrent;
46
using System.Collections.Generic;
@@ -9,7 +11,7 @@
911
using System.Runtime.CompilerServices;
1012
using System.Threading;
1113
using static System.Linq.Expressions.Expression;
12-
14+
//using static FastExpressionCompiler.LightExpression.Expression;
1315
namespace Open.Text;
1416

1517
/// <summary>
@@ -83,7 +85,7 @@ static Func<TEnum, string> GetEnumNameDelegate()
8385
Default(tResult)
8486
),
8587
null,
86-
Values.Select(v => SwitchCase(
88+
Values.OrderBy(v => string.Intern(v.ToString())).Select(v => SwitchCase(
8789
Constant(string.Intern(v.ToString())),
8890
Constant(v)
8991
)).ToArray()
@@ -93,41 +95,46 @@ static Func<TEnum, string> GetEnumNameDelegate()
9395
private static Func<TEnum, string>? _nameLookup;
9496
internal static Func<TEnum, string> NameLookup
9597
=> LazyInitializer.EnsureInitialized(ref _nameLookup,
96-
() => GetEnumNameDelegate())!;
98+
GetEnumNameDelegate)!;
9799
internal static Func<string, ValueLookupResult> GetEnumTryParseDelegate()
98100
{
99101
var valueParam = Parameter(typeof(string), "value");
102+
var lengthParam = Variable(typeof(int), "length");
100103
var defaultExpression = CreateNewTuple(false, default!);
101104
var enumValues = Values.Select(v => (value: v, name: string.Intern(v.ToString())));
102-
var nameGroups = enumValues.GroupBy(v => v.name.Length);
105+
var nameGroups = enumValues.GroupBy(v => v.name.Length).OrderBy(g => g.Key);
103106
var lengthCheckCases = nameGroups.Select(group =>
104107
SwitchCase(
105108
Switch(
106109
valueParam,
107110
defaultExpression,
108111
null,
109-
group.Select(v => SwitchCase(
112+
group.OrderBy(v => v.name).Select(v => SwitchCase(
110113
CreateNewTuple(true, v.value),
111114
Constant(v.name)
112-
)).ToArray()
115+
))
116+
.ToArray()
113117
),
114118
Constant(group.Key)
115119
)
116120
).ToArray();
117121

118122
return Lambda<Func<string, ValueLookupResult>>(
119-
Switch(
120-
Property(valueParam, nameof(string.Length)),
121-
defaultExpression,
122-
null,
123-
lengthCheckCases
123+
Block(new[] { lengthParam },
124+
Assign(lengthParam, Property(valueParam, nameof(string.Length))),
125+
Switch(
126+
lengthParam,
127+
defaultExpression,
128+
null,
129+
lengthCheckCases
130+
)
124131
),
125132
valueParam
126133
).Compile();
127134

128135
static Expression CreateNewTuple(bool success, TEnum value)
129136
=> New(
130-
typeof(ValueLookupResult).GetConstructor(new[] { typeof(bool), typeof(TEnum) }),
137+
typeof(ValueLookupResult).GetConstructor(new[] { typeof(bool), typeof(TEnum) })!,
131138
Constant(success),
132139
Constant(value)
133140
);
@@ -153,8 +160,12 @@ public void Deconstruct(out bool success, out TEnum value)
153160

154161
private static Func<string, ValueLookupResult>? _valueLookup;
155162
internal static Func<string, ValueLookupResult> ValueLookup
156-
=> LazyInitializer.EnsureInitialized(ref _valueLookup,
157-
() => GetEnumTryParseDelegate())!;
163+
=> LazyInitializer.EnsureInitialized(ref _valueLookup, GetEnumTryParseDelegate)!;
164+
165+
private static IReadOnlyDictionary<string, TEnum>? _ignoreCaseLookup;
166+
internal static IReadOnlyDictionary<string, TEnum> IgnoreCaseLookup
167+
=> LazyInitializer.EnsureInitialized(ref _ignoreCaseLookup,
168+
() => Values.ToDictionary(e => Enum.GetName(typeof(TEnum), e)!, e => e, StringComparer.OrdinalIgnoreCase))!;
158169

159170
static Entry[]?[] CreateLookup()
160171
{
@@ -484,6 +495,7 @@ public static class EnumValue
484495
/// <returns>The enum that represents the string <paramref name="value"/> provided.</returns>
485496
/// <exception cref="ArgumentException">Requested <paramref name="value"/> was not found.</exception>
486497
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
498+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
487499
public static TEnum Parse<TEnum>(string value)
488500
where TEnum : Enum
489501
{
@@ -493,6 +505,7 @@ public static TEnum Parse<TEnum>(string value)
493505
}
494506

495507
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, out TEnum)"/>
508+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
496509
public static bool TryParse<TEnum>(string value, out TEnum e)
497510
where TEnum : Enum
498511
{
@@ -505,30 +518,54 @@ public static bool TryParse<TEnum>(string value, out TEnum e)
505518
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
506519
/// <exception cref="ArgumentException">Requested <paramref name="value"/> was not found.</exception>
507520
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
521+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
508522
public static TEnum Parse<TEnum>(StringSegment value)
509523
where TEnum : Enum
510524
=> TryParse<TEnum>(value, false, out var e) ? e
511525
: throw new ArgumentException(string.Format(NotFoundMessage, value), nameof(value));
512526

513527
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
528+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
514529
public static TEnum Parse<TEnum>(string value, bool ignoreCase)
515530
where TEnum : Enum
516531
=> ignoreCase
517-
? TryParse<TEnum>(value, ignoreCase, out var e) ? e
532+
? EnumValue<TEnum>.IgnoreCaseLookup.TryGetValue(value, out var e) ? e
518533
: throw new ArgumentException(string.Format(NotFoundMessage, value), nameof(value))
519534
: Parse<TEnum>(value);
520535

521536
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
537+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
522538
public static TEnum Parse<TEnum>(StringSegment value, bool ignoreCase)
523539
where TEnum : Enum
524-
=> TryParse<TEnum>(value, ignoreCase, out var e) ? e
525-
: throw new ArgumentException(string.Format(NotFoundMessage, value), nameof(value));
540+
{
541+
var buffer = value.Buffer ?? throw new ArgumentNullException(nameof(value));
542+
return value.Length == buffer.Length
543+
? Parse<TEnum>(value.Buffer, ignoreCase)
544+
: TryParse<TEnum>(value, ignoreCase, out var e)
545+
? e : throw new ArgumentException(string.Format(NotFoundMessage, value), nameof(value));
546+
}
526547

527548
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
549+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
528550
public static bool TryParse<TEnum>(StringSegment value, out TEnum e)
529551
where TEnum : Enum
530552
=> TryParse(value, false, out e);
531553

554+
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
555+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
556+
public static bool TryParse<TEnum>(string name, bool ignoreCase, out TEnum e)
557+
where TEnum : Enum
558+
=> ignoreCase
559+
? EnumValue<TEnum>.IgnoreCaseLookup.TryGetValue(name, out e)
560+
: TryParse(name, out e);
561+
562+
/// <inheritdoc cref="TryParse{TEnum}(StringSegment, bool, out TEnum)"/>
563+
/// <remarks>Can be slightly faster than other ignore-case methods.</remarks>
564+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
565+
public static bool TryParseIgnoreCase<TEnum>(string name, out TEnum e)
566+
where TEnum : Enum
567+
=> EnumValue<TEnum>.IgnoreCaseLookup.TryGetValue(name, out e);
568+
532569
/// <summary>
533570
/// Converts the string representation of the name of one or more enumerated constants to an equivalent enumerated object.
534571
/// </summary>
@@ -543,8 +580,9 @@ public static bool TryParse<TEnum>(StringSegment name, bool ignoreCase, out TEnu
543580
if (len == 0) goto notFound;
544581

545582
// If this is a string, use the optimized version.
546-
if (!ignoreCase && name.Buffer!.Length == len)
547-
return TryParse(name.Buffer, out e);
583+
string buffer = name.Buffer!;
584+
if (buffer.Length == len)
585+
return TryParse(buffer, ignoreCase, out e);
548586

549587
var lookup = EnumValue<TEnum>.Lookup;
550588
if (len >= lookup.Length) goto notFound;

Source/Open.Text.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<RepositoryUrl>https://github.com/Open-NET-Libraries/Open.Text</RepositoryUrl>
2020
<RepositoryType>git</RepositoryType>
2121
<PackageTags>string, span, enum, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions, stringcomparable, spancomparable, stringsegment, splitassegment</PackageTags>
22-
<Version>6.6.2</Version>
22+
<Version>6.6.3</Version>
2323
<PackageReleaseNotes></PackageReleaseNotes>
2424
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2525
<PublishRepositoryUrl>true</PublishRepositoryUrl>

0 commit comments

Comments
 (0)