From 7a1664e2932977390632e59bdf9569340499e869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 18 Sep 2023 22:55:00 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Improve=20`Debug`,=20`Register`,=20?= =?UTF-8?q?`SetOption`=20and=20`Position`=20UCI=20command=20parsing=20perf?= =?UTF-8?q?ormance=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using `Span.Split` instead of `string.Split`, which reduces allocations --- src/Lynx.Benchmark/DebugCommand.cs | 105 +++++++++ src/Lynx.Benchmark/RegisterCommand.cs | 224 +++++++++++++++++++ src/Lynx/Engine.cs | 2 +- src/Lynx/LynxDriver.cs | 59 +++-- src/Lynx/UCI/Commands/GUI/DebugCommand.cs | 27 ++- src/Lynx/UCI/Commands/GUI/PositionCommand.cs | 7 +- src/Lynx/UCI/Commands/GUI/RegisterCommand.cs | 26 ++- 7 files changed, 407 insertions(+), 43 deletions(-) create mode 100644 src/Lynx.Benchmark/DebugCommand.cs create mode 100644 src/Lynx.Benchmark/RegisterCommand.cs diff --git a/src/Lynx.Benchmark/DebugCommand.cs b/src/Lynx.Benchmark/DebugCommand.cs new file mode 100644 index 000000000..3e08321af --- /dev/null +++ b/src/Lynx.Benchmark/DebugCommand.cs @@ -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 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 command) + { + const string on = "on"; + const string off = "off"; + + Span 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 command) + { + const string on = "on"; + const string off = "off"; + + Span 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); + } + } +} diff --git a/src/Lynx.Benchmark/RegisterCommand.cs b/src/Lynx.Benchmark/RegisterCommand.cs new file mode 100644 index 000000000..8ca6f586d --- /dev/null +++ b/src/Lynx.Benchmark/RegisterCommand.cs @@ -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 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 command) + { + const string later = "later"; + const string name = "name"; + const string code = "code"; + + Span 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 command) + { + const string later = "later"; + const string name = "name"; + const string code = "code"; + + Span 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(); + } + } + } +} \ No newline at end of file diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index ca4f965e9..73d491a09 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -56,7 +56,7 @@ public void NewGame() InitializeTT(); } - public void AdjustPosition(string rawPositionCommand) + public void AdjustPosition(ReadOnlySpan rawPositionCommand) { Game = PositionCommand.ParseGame(rawPositionCommand); _isNewGameComing = false; diff --git a/src/Lynx/LynxDriver.cs b/src/Lynx/LynxDriver.cs index 4bbc510ce..975ae8992 100644 --- a/src/Lynx/LynxDriver.cs +++ b/src/Lynx/LynxDriver.cs @@ -13,7 +13,6 @@ public sealed class LynxDriver private readonly Channel _engineWriter; private readonly Engine _engine; private readonly Logger _logger; - private static readonly string[] _none = new[] { "none" }; public LynxDriver(ChannelReader uciReader, Channel engineWriter, Engine engine) { @@ -35,6 +34,14 @@ private static void InitializeStaticClasses() public async Task Run(CancellationToken cancellationToken) { + static ReadOnlySpan ExtractCommandItems(string rawCommand) + { + var span = rawCommand.AsSpan(); + Span items = stackalloc Range[2]; + span.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries); + + return span[items[0]]; + } try { while (await _uciReader.WaitToReadAsync(cancellationToken) && !cancellationToken.IsCancellationRequested) @@ -45,8 +52,7 @@ public async Task Run(CancellationToken cancellationToken) { _logger.Debug("[GUI]\t{0}", rawCommand); - var commandItems = rawCommand.Split(' ', StringSplitOptions.RemoveEmptyEntries); - switch (commandItems[0].ToLowerInvariant()) + switch (ExtractCommandItems(rawCommand)) { case DebugCommand.Id: HandleDebug(rawCommand); @@ -70,7 +76,7 @@ public async Task Run(CancellationToken cancellationToken) HandleRegister(rawCommand); break; case SetOptionCommand.Id: - HandleSetOption(rawCommand, commandItems); + HandleSetOption(rawCommand); break; case StopCommand.Id: HandleStop(); @@ -118,7 +124,7 @@ public async Task Run(CancellationToken cancellationToken) #region Command handlers - private void HandlePosition(string command) + private void HandlePosition(ReadOnlySpan command) { #if DEBUG _engine.Game.CurrentPosition.Print(); @@ -165,18 +171,24 @@ private void HandlePonderHit() } } - private void HandleSetOption(string command, string[] commandItems) + private void HandleSetOption(ReadOnlySpan command) { - if (commandItems.Length < 3 || !string.Equals(commandItems[1], "name", StringComparison.OrdinalIgnoreCase)) + Span commandItems = stackalloc Range[5]; + var length = command.Split(commandItems, ' ', StringSplitOptions.RemoveEmptyEntries); + + if (commandItems[2].Start.Equals(commandItems[2].End) || !command[commandItems[1]].Equals("name", StringComparison.OrdinalIgnoreCase)) { return; } - switch (commandItems[2].ToLowerInvariant()) + Span lowerCaseFirstWord = stackalloc char[command[commandItems[2]].Length]; + command[commandItems[2]].ToLowerInvariant(lowerCaseFirstWord); + + switch (lowerCaseFirstWord) { case "ponder": { - if (commandItems.Length > 4 && bool.TryParse(commandItems[4], out var value)) + if (length > 4 && bool.TryParse(command[commandItems[4]], out var value)) { Configuration.IsPonder = value; } @@ -185,7 +197,7 @@ private void HandleSetOption(string command, string[] commandItems) } case "uci_analysemode": { - if (commandItems.Length > 4 && bool.TryParse(commandItems[4], out var value)) + if (length > 4 && bool.TryParse(command[commandItems[4]], out var value)) { Configuration.UCI_AnalyseMode = value; } @@ -193,7 +205,7 @@ private void HandleSetOption(string command, string[] commandItems) } case "depth": { - if (commandItems.Length > 4 && int.TryParse(commandItems[4], out var value)) + if (length > 4 && int.TryParse(command[commandItems[4]], out var value)) { Configuration.EngineSettings.DefaultMaxDepth = value; } @@ -201,7 +213,7 @@ private void HandleSetOption(string command, string[] commandItems) } case "hash": { - if (commandItems.Length > 4 && int.TryParse(commandItems[4], out var value)) + if (length > 4 && int.TryParse(command[commandItems[4]], out var value)) { Configuration.Hash = Math.Clamp(value, 0, 1024); } @@ -209,23 +221,26 @@ private void HandleSetOption(string command, string[] commandItems) } case "uci_opponent": { - if (commandItems.Length > 4) + const string none = "none "; + if (length > 4) { - _logger.Info("Game against {0}", string.Join(' ', commandItems.Skip(4).Except(_none))); + var opponent = command[commandItems[4].Start.Value..].ToString(); + + _logger.Info("Game against {0}", opponent.Replace(none, string.Empty)); } break; } case "uci_engineabout": { - if (commandItems.Length > 4) + if (length > 4) { - _logger.Info("UCI_EngineAbout: {0}", string.Join(' ', commandItems.Skip(4))); + _logger.Info("UCI_EngineAbout: {0}", command[commandItems[4].Start.Value..].ToString()); } break; } case "onlinetablebaseinrootpositions": { - if (commandItems.Length > 4 && bool.TryParse(commandItems[4], out var value)) + if (length > 4 && bool.TryParse(command[commandItems[4]], out var value)) { Configuration.EngineSettings.UseOnlineTablebaseInRootPositions = value; } @@ -233,7 +248,7 @@ private void HandleSetOption(string command, string[] commandItems) } case "onlinetablebaseinsearch": { - if (commandItems.Length > 4 && bool.TryParse(commandItems[4], out var value)) + if (length > 4 && bool.TryParse(command[commandItems[4]], out var value)) { Configuration.EngineSettings.UseOnlineTablebaseInSearch = value; } @@ -241,7 +256,7 @@ private void HandleSetOption(string command, string[] commandItems) } case "threads": { - if (commandItems.Length > 4 && int.TryParse(commandItems[4], out var value)) + if (length > 4 && int.TryParse(command[commandItems[4]], out var value)) { if (value != 1) { @@ -251,7 +266,7 @@ private void HandleSetOption(string command, string[] commandItems) break; } default: - _logger.Warn("Unsupported option: {0}", command); + _logger.Warn("Unsupported option: {0}", command.ToString()); break; } } @@ -265,7 +280,7 @@ private void HandleNewGame() _engine.NewGame(); } - private static void HandleDebug(string command) => Configuration.IsDebug = DebugCommand.Parse(command); + private static void HandleDebug(ReadOnlySpan command) => Configuration.IsDebug = DebugCommand.Parse(command); private void HandleQuit() { @@ -276,7 +291,7 @@ private void HandleQuit() _engineWriter.Writer.Complete(); } - private void HandleRegister(string rawCommand) => _engine.Registration = new RegisterCommand(rawCommand); + private void HandleRegister(ReadOnlySpan rawCommand) => _engine.Registration = new RegisterCommand(rawCommand); private async Task HandlePerft(string rawCommand) { diff --git a/src/Lynx/UCI/Commands/GUI/DebugCommand.cs b/src/Lynx/UCI/Commands/GUI/DebugCommand.cs index 9e33f15cb..e22c1acf4 100644 --- a/src/Lynx/UCI/Commands/GUI/DebugCommand.cs +++ b/src/Lynx/UCI/Commands/GUI/DebugCommand.cs @@ -12,11 +12,28 @@ public sealed class DebugCommand : GUIBaseCommand { public const string Id = "debug"; - public static bool Parse(string command) + /// + /// Parse debug command + /// + /// + /// + /// true if debug command sent 'on' + /// false if debug command sent 'off' + /// if something else was sent + /// + /// + public static bool Parse(ReadOnlySpan command) { - return string.Equals( - "on", - command.Split(' ', System.StringSplitOptions.RemoveEmptyEntries)[1], - System.StringComparison.OrdinalIgnoreCase); + const string on = "on"; + const string off= "off"; + + Span 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); } } diff --git a/src/Lynx/UCI/Commands/GUI/PositionCommand.cs b/src/Lynx/UCI/Commands/GUI/PositionCommand.cs index a1a6896f6..98fe0920c 100644 --- a/src/Lynx/UCI/Commands/GUI/PositionCommand.cs +++ b/src/Lynx/UCI/Commands/GUI/PositionCommand.cs @@ -1,7 +1,6 @@ using Lynx.Model; using NLog; using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; namespace Lynx.UCI.Commands.GUI; @@ -22,12 +21,10 @@ public sealed class PositionCommand : GUIBaseCommand private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public static Game ParseGame(string positionCommand) + public static Game ParseGame(ReadOnlySpan positionCommandSpan) { try { - var positionCommandSpan = positionCommand.AsSpan(); - // We divide the position command in these two sections: // "position startpos ||" // "position startpos || moves e2e4 e7e5" @@ -57,7 +54,7 @@ public static Game ParseGame(string positionCommand) } catch (Exception e) { - _logger.Error(e, "Error parsing position command '{0}'", positionCommand); + _logger.Error(e, "Error parsing position command '{0}'", positionCommandSpan.ToString()); return new Game(); } } diff --git a/src/Lynx/UCI/Commands/GUI/RegisterCommand.cs b/src/Lynx/UCI/Commands/GUI/RegisterCommand.cs index df3f2eef9..d04b0e172 100644 --- a/src/Lynx/UCI/Commands/GUI/RegisterCommand.cs +++ b/src/Lynx/UCI/Commands/GUI/RegisterCommand.cs @@ -28,11 +28,16 @@ public sealed class RegisterCommand : GUIBaseCommand public string Code { get; } = string.Empty; - public RegisterCommand(string command) + public RegisterCommand(ReadOnlySpan command) { - var items = command.Split(' ', System.StringSplitOptions.RemoveEmptyEntries); + const string later = "later"; + const string name = "name"; + const string code = "code"; - if (string.Equals("later", items[1], System.StringComparison.OrdinalIgnoreCase)) + Span items = stackalloc Range[6]; + var itemsLength = command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (command[items[1]].Equals(later, StringComparison.OrdinalIgnoreCase)) { Later = true; return; @@ -40,16 +45,17 @@ public RegisterCommand(string command) var sb = new StringBuilder(); - foreach (var item in items[1..]) + for (int i = 1; i < itemsLength; ++i) { - if (string.Equals("name", item, System.StringComparison.OrdinalIgnoreCase)) + var item = command[items[i]]; + if (item.Equals(name, StringComparison.OrdinalIgnoreCase)) { - Code = sb.ToString().TrimEnd(); + Code = sb.ToString(); sb.Clear(); } - else if (string.Equals("code", item, System.StringComparison.OrdinalIgnoreCase)) + else if (item.Equals(code, StringComparison.OrdinalIgnoreCase)) { - Name = sb.ToString().TrimEnd(); + Name = sb.ToString(); sb.Clear(); } else @@ -61,11 +67,11 @@ public RegisterCommand(string command) if (string.IsNullOrEmpty(Name)) { - Name = sb.ToString().TrimEnd(); + Name = sb.ToString(); } else { - Code = sb.ToString().TrimEnd(); + Code = sb.ToString(); } } }