Skip to content

Commit

Permalink
⚡ Improve Debug, Register, SetOption and Position UCI command…
Browse files Browse the repository at this point in the history
… parsing performance (#411)

Using `Span.Split` instead of `string.Split`, which reduces allocations
  • Loading branch information
eduherminio committed Sep 18, 2023
1 parent e5096e0 commit 7a1664e
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 43 deletions.
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

0 comments on commit 7a1664e

Please sign in to comment.