-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⚡ Improve
Debug
, Register
, SetOption
and Position
UCI command…
… parsing performance (#411) Using `Span.Split` instead of `string.Split`, which reduces allocations
- Loading branch information
1 parent
e5096e0
commit 7a1664e
Showing
7 changed files
with
407 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* | ||
* BenchmarkDotNet v0.13.8, Windows 10 (10.0.19045.3448/22H2/2022Update) | ||
* Intel Core i7-5500U CPU 2.40GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores | ||
* .NET SDK 8.0.100-rc.1.23463.5 | ||
* [Host] : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2 | ||
* DefaultJob : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2 | ||
* | ||
* | ||
* | Method | command | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | | ||
* |------------ |---------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| | ||
* | StringSplit | debug off | 77.09 ns | 1.584 ns | 1.824 ns | 1.00 | 0.00 | 0.0497 | 104 B | 1.00 | | ||
* | SpanSplit | debug off | 44.06 ns | 0.591 ns | 0.524 ns | 0.57 | 0.01 | - | - | 0.00 | | ||
* | SpanSplit2 | debug off | 46.36 ns | 0.739 ns | 0.617 ns | 0.60 | 0.02 | - | - | 0.00 | | ||
* | | | | | | | | | | | | ||
* | StringSplit | debug on | 73.92 ns | 1.373 ns | 1.217 ns | 1.00 | 0.00 | 0.0497 | 104 B | 1.00 | | ||
* | SpanSplit | debug on | 38.15 ns | 0.732 ns | 0.649 ns | 0.52 | 0.01 | - | - | 0.00 | | ||
* | SpanSplit2 | debug on | 37.50 ns | 0.574 ns | 0.537 ns | 0.51 | 0.01 | - | - | 0.00 | | ||
* | | | | | | | | | | | | ||
* | StringSplit | debug onf | 79.91 ns | 1.378 ns | 1.587 ns | 1.00 | 0.00 | 0.0497 | 104 B | 1.00 | | ||
* | SpanSplit | debug onf | 48.67 ns | 0.638 ns | 0.533 ns | 0.61 | 0.01 | - | - | 0.00 | | ||
* | SpanSplit2 | debug onf | 40.53 ns | 0.550 ns | 0.429 ns | 0.51 | 0.01 | - | - | 0.0 | // I forgot a !sign, hence the diff here | ||
* | ||
*/ | ||
|
||
using BenchmarkDotNet.Attributes; | ||
using Lynx.UCI.Commands; | ||
|
||
namespace Lynx.Benchmark; | ||
public class DebugCommandBenchmark : BaseBenchmark | ||
{ | ||
public static IEnumerable<string> Data => new[] | ||
{ | ||
"debug on", | ||
"debug off", | ||
"debug onf", | ||
}; | ||
|
||
[Benchmark(Baseline = true)] | ||
[ArgumentsSource(nameof(Data))] | ||
public bool StringSplit(string command) => DebugCommandBenchmark_DebugCommandStringSplit.Parse(command); | ||
|
||
[Benchmark] | ||
[ArgumentsSource(nameof(Data))] | ||
public bool SpanSplit(string command) => DebugCommandBenchmark_DebugCommandSpanSplit.Parse(command); | ||
|
||
[Benchmark] | ||
[ArgumentsSource(nameof(Data))] | ||
public bool SpanSplit2(string command) => DebugCommandBenchmark_DebugCommandSpanSplit2.Parse(command); | ||
|
||
public sealed class DebugCommandBenchmark_DebugCommandStringSplit : GUIBaseCommand | ||
{ | ||
public const string Id = "debug"; | ||
|
||
public static bool Parse(string command) | ||
{ | ||
const string on = "on"; | ||
const string off = "off"; | ||
|
||
var state = command.Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]; | ||
|
||
return state.Equals(on, StringComparison.OrdinalIgnoreCase) | ||
|| (!state.Equals(off, StringComparison.OrdinalIgnoreCase) | ||
&& Configuration.IsDebug); | ||
} | ||
} | ||
|
||
public sealed class DebugCommandBenchmark_DebugCommandSpanSplit : GUIBaseCommand | ||
{ | ||
public const string Id = "debug"; | ||
|
||
public static bool Parse(ReadOnlySpan<char> command) | ||
{ | ||
const string on = "on"; | ||
const string off = "off"; | ||
|
||
Span<Range> items = stackalloc Range[2]; | ||
command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries); | ||
|
||
return command[items[1]].Equals(on, StringComparison.OrdinalIgnoreCase) | ||
|| (!command[items[1]].Equals(off, StringComparison.OrdinalIgnoreCase) | ||
&& Configuration.IsDebug); | ||
} | ||
} | ||
|
||
public sealed class DebugCommandBenchmark_DebugCommandSpanSplit2 : GUIBaseCommand | ||
{ | ||
public const string Id = "debug"; | ||
|
||
public static bool Parse(ReadOnlySpan<char> command) | ||
{ | ||
const string on = "on"; | ||
const string off = "off"; | ||
|
||
Span<Range> items = stackalloc Range[2]; | ||
command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries); | ||
|
||
var debugValue = command[items[1]]; | ||
|
||
return debugValue.Equals(on, StringComparison.OrdinalIgnoreCase) | ||
|| (!debugValue.Equals(off, StringComparison.OrdinalIgnoreCase) | ||
&& Configuration.IsDebug); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
/* | ||
* | ||
* BenchmarkDotNet v0.13.8, Windows 10 (10.0.19045.3448/22H2/2022Update) | ||
* Intel Core i7-5500U CPU 2.40GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores | ||
* .NET SDK 8.0.100-rc.1.23463.5 | ||
* [Host] : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2 | ||
* DefaultJob : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2 | ||
* | ||
* | ||
* | Method | command | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | | ||
* |---------------- |--------------------- |----------:|----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| | ||
* | StringSplit | register late | 172.04 ns | 3.535 ns | 6.191 ns | 170.74 ns | 1.00 | 0.00 | 0.1683 | 352 B | 1.00 | | ||
* | SpanSplit | register late | 106.20 ns | 1.335 ns | 1.115 ns | 105.79 ns | 0.62 | 0.02 | 0.0842 | 176 B | 0.50 | | ||
* | SpanSplitStruct | register late | 97.06 ns | 1.711 ns | 1.517 ns | 97.47 ns | 0.57 | 0.02 | 0.0650 | 136 B | 0.39 | | ||
* | | | | | | | | | | | | | ||
* | StringSplit | regis(...)74324 [98] | 726.40 ns | 27.308 ns | 72.890 ns | 692.78 ns | 1.00 | 0.00 | 0.8717 | 1824 B | 1.00 | | ||
* | SpanSplit | regis(...)74324 [98] | 338.85 ns | 6.824 ns | 8.123 ns | 339.86 ns | 0.43 | 0.05 | 0.3748 | 784 B | 0.43 | | ||
* | SpanSplitStruct | regis(...)74324 [98] | 311.02 ns | 6.152 ns | 5.453 ns | 310.14 ns | 0.42 | 0.04 | 0.3557 | 744 B | 0.41 | | ||
* | | | | | | | | | | | | | ||
* | StringSplit | regis(...)74324 [41] | 336.63 ns | 6.454 ns | 6.038 ns | 338.06 ns | 1.00 | 0.00 | 0.3328 | 696 B | 1.00 | | ||
* | SpanSplit | regis(...)74324 [41] | 199.10 ns | 4.035 ns | 3.577 ns | 199.03 ns | 0.59 | 0.01 | 0.1147 | 240 B | 0.34 | | ||
* | SpanSplitStruct | regis(...)74324 [41] | 204.13 ns | 4.136 ns | 3.667 ns | 202.78 ns | 0.61 | 0.02 | 0.0956 | 200 B | 0.29 | | ||
* | | | | | | | | | | | | | ||
* | StringSplit | regis(...)74324 [39] | 333.95 ns | 6.689 ns | 8.459 ns | 333.40 ns | 1.00 | 0.00 | 0.3290 | 688 B | 1.00 | | ||
* | SpanSplit | regis(...)74324 [39] | 200.44 ns | 3.881 ns | 4.313 ns | 199.69 ns | 0.60 | 0.02 | 0.1147 | 240 B | 0.35 | | ||
* | SpanSplitStruct | regis(...)74324 [39] | 193.11 ns | 2.168 ns | 2.028 ns | 193.30 ns | 0.58 | 0.02 | 0.0956 | 200 B | 0.29 | | ||
* | ||
*/ | ||
|
||
using BenchmarkDotNet.Attributes; | ||
using Lynx.UCI.Commands; | ||
using System.Text; | ||
|
||
namespace Lynx.Benchmark; | ||
public class RegisterCommandBenchmark : BaseBenchmark | ||
{ | ||
public static IEnumerable<string> Data => new[] | ||
{ | ||
"register late", | ||
"register name Stefan MK code 4359874324", | ||
"register name Lynx 0.16.0 code 4359874324", | ||
"register name Lynx 0.16.0 by eduherminio, check https://github.com/lync-chess/lynx code 4359874324", | ||
}; | ||
|
||
[Benchmark(Baseline = true)] | ||
[ArgumentsSource(nameof(Data))] | ||
public RegisterCommandBenchmark_RegisterCommandStringSplit StringSplit(string command) => new RegisterCommandBenchmark_RegisterCommandStringSplit(command); | ||
|
||
[Benchmark] | ||
[ArgumentsSource(nameof(Data))] | ||
public RegisterCommandBenchmark_RegisterCommandSpanSplit SpanSplit(string command) => new RegisterCommandBenchmark_RegisterCommandSpanSplit(command); | ||
|
||
[Benchmark] | ||
[ArgumentsSource(nameof(Data))] | ||
public RegisterCommandBenchmark_RegisterCommandSpanSplitStruct SpanSplitStruct(string command) => new RegisterCommandBenchmark_RegisterCommandSpanSplitStruct(command); | ||
|
||
public sealed class RegisterCommandBenchmark_RegisterCommandStringSplit : GUIBaseCommand | ||
{ | ||
public const string Id = "register"; | ||
|
||
public bool Later { get; } | ||
|
||
public string Name { get; } = string.Empty; | ||
|
||
public string Code { get; } = string.Empty; | ||
|
||
public RegisterCommandBenchmark_RegisterCommandStringSplit(string command) | ||
{ | ||
var items = command.Split(' ', StringSplitOptions.RemoveEmptyEntries); | ||
|
||
if (string.Equals("later", items[1], StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Later = true; | ||
return; | ||
} | ||
|
||
var sb = new StringBuilder(); | ||
|
||
foreach (var item in items[1..]) | ||
{ | ||
if (string.Equals("name", item, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Code = sb.ToString().TrimEnd(); | ||
sb.Clear(); | ||
} | ||
else if (string.Equals("code", item, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Name = sb.ToString().TrimEnd(); | ||
sb.Clear(); | ||
} | ||
else | ||
{ | ||
sb.Append(item); | ||
sb.Append(' '); | ||
} | ||
} | ||
|
||
if (string.IsNullOrEmpty(Name)) | ||
{ | ||
Name = sb.ToString().TrimEnd(); | ||
} | ||
else | ||
{ | ||
Code = sb.ToString().TrimEnd(); | ||
} | ||
} | ||
} | ||
|
||
public sealed class RegisterCommandBenchmark_RegisterCommandSpanSplit : GUIBaseCommand | ||
{ | ||
public const string Id = "register"; | ||
|
||
public bool Later { get; } | ||
|
||
public string Name { get; } = string.Empty; | ||
|
||
public string Code { get; } = string.Empty; | ||
|
||
public RegisterCommandBenchmark_RegisterCommandSpanSplit(ReadOnlySpan<char> command) | ||
{ | ||
const string later = "later"; | ||
const string name = "name"; | ||
const string code = "code"; | ||
|
||
Span<Range> items = stackalloc Range[6]; | ||
var itemsLength = command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||
|
||
if (command[items[1]].Equals(later, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Later = true; | ||
return; | ||
} | ||
|
||
var sb = new StringBuilder(); | ||
|
||
for (int i = 1; i < itemsLength; ++i) | ||
{ | ||
var item = command[items[i]]; | ||
if (item.Equals(name, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Code = sb.ToString(); | ||
sb.Clear(); | ||
} | ||
else if (item.Equals(code, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Name = sb.ToString(); | ||
sb.Clear(); | ||
} | ||
else | ||
{ | ||
sb.Append(item); | ||
sb.Append(' '); | ||
} | ||
} | ||
|
||
if (string.IsNullOrEmpty(Name)) | ||
{ | ||
Name = sb.ToString(); | ||
} | ||
else | ||
{ | ||
Code = sb.ToString(); | ||
} | ||
} | ||
} | ||
|
||
public readonly struct RegisterCommandBenchmark_RegisterCommandSpanSplitStruct | ||
{ | ||
public const string Id = "register"; | ||
|
||
public bool Later { get; } | ||
|
||
public string Name { get; } = string.Empty; | ||
|
||
public string Code { get; } = string.Empty; | ||
|
||
public RegisterCommandBenchmark_RegisterCommandSpanSplitStruct(ReadOnlySpan<char> command) | ||
{ | ||
const string later = "later"; | ||
const string name = "name"; | ||
const string code = "code"; | ||
|
||
Span<Range> items = stackalloc Range[6]; | ||
var itemsLength = command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||
|
||
if (command[items[1]].Equals(later, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Later = true; | ||
return; | ||
} | ||
|
||
var sb = new StringBuilder(); | ||
|
||
for (int i = 1; i < itemsLength; ++i) | ||
{ | ||
var item = command[items[i]]; | ||
if (item.Equals(name, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Code = sb.ToString(); | ||
sb.Clear(); | ||
} | ||
else if (item.Equals(code, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Name = sb.ToString(); | ||
sb.Clear(); | ||
} | ||
else | ||
{ | ||
sb.Append(item); | ||
sb.Append(' '); | ||
} | ||
} | ||
|
||
if (string.IsNullOrEmpty(Name)) | ||
{ | ||
Name = sb.ToString(); | ||
} | ||
else | ||
{ | ||
Code = sb.ToString(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.