Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡ Improve Debug, Register, SetOption and Position UCI command parsing performance #411

Merged
merged 1 commit into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/Lynx.Benchmark/DebugCommand.cs
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);
}
}
}
224 changes: 224 additions & 0 deletions src/Lynx.Benchmark/RegisterCommand.cs
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();
}
}
}
}
2 changes: 1 addition & 1 deletion src/Lynx/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void NewGame()
InitializeTT();
}

public void AdjustPosition(string rawPositionCommand)
public void AdjustPosition(ReadOnlySpan<char> rawPositionCommand)
{
Game = PositionCommand.ParseGame(rawPositionCommand);
_isNewGameComing = false;
Expand Down