From be19d9aae6c598b1a911e02200f4f2da91242a16 Mon Sep 17 00:00:00 2001 From: Harley Holt Date: Tue, 25 Oct 2022 12:08:35 -0700 Subject: [PATCH] fix: Optimizes Point2D by implementing ISpanFormattable (#1203) Point2D implements a basic TryFormat function. Only a single format is supported: "(X, Y)" where X and Y are base 10 integers. Uses recent improvements in string interpolation to write the characters to the destination without first allocating memory for boxed arguments or intermediate strings. This is a partial solution to issue #1067. A similar solution well be implemented in other classes in Geometry if it looks promising. Benchmarks --- https://github.com/harleyholt/ModernUOBenchmarks/blob/d6f8dabf3c813937e65874ea82004701be0153c4/Program.cs Before change: ``` BenchmarkDotNet=v0.13.2, OS=ubuntu 22.04 Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores .NET SDK=6.0.402 [Host] : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 | Method | Mean | Error | StdDev | Gen0 | Allocated | |--------------------------------- |----------:|---------:|---------:|-------:|----------:| | CallToString | 44.27 ns | 0.068 ns | 0.063 ns | 0.0063 | 40 B | | InterpolatedString | 110.64 ns | 0.316 ns | 0.295 ns | 0.0126 | 80 B | | InterpolatedStringMultiplePoints | 211.36 ns | 0.349 ns | 0.326 ns | 0.0293 | 184 B | ``` After change: ``` BenchmarkDotNet=v0.13.2, OS=ubuntu 22.04 Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores .NET SDK=6.0.402 [Host] : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 | Method | Mean | Error | StdDev | Gen0 | Allocated | |--------------------------------- |----------:|---------:|---------:|-------:|----------:| | CallToString | 55.13 ns | 0.826 ns | 0.772 ns | 0.0063 | 40 B | | InterpolatedString | 55.73 ns | 0.954 ns | 0.892 ns | 0.0063 | 40 B | | InterpolatedStringMultiplePoints | 105.42 ns | 0.151 ns | 0.134 ns | 0.0101 | 64 B | ``` --- .../Tests/Geometry/Point2DTests.cs | 17 ++++++++++++ Projects/Server/Geometry/Point2D.cs | 26 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Geometry/Point2DTests.cs diff --git a/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs new file mode 100644 index 000000000..251beff74 --- /dev/null +++ b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs @@ -0,0 +1,17 @@ +using System; +using Xunit; + +namespace Server.Tests +{ + public class Point2DTests + { + [Fact] + public void TestToString() + { + Assert.Equal("(0, 0)", new Point2D(0, 0).ToString()); + Assert.Equal("(1, 1)", new Point2D(1, 1).ToString()); + Assert.Equal($"({Int32.MaxValue}, {Int32.MaxValue})", new Point2D(Int32.MaxValue, Int32.MaxValue).ToString()); + Assert.Equal($"({Int32.MinValue}, {Int32.MinValue})", new Point2D(Int32.MinValue, Int32.MinValue).ToString()); + } + } +} diff --git a/Projects/Server/Geometry/Point2D.cs b/Projects/Server/Geometry/Point2D.cs index 63bee4c40..b0c407853 100644 --- a/Projects/Server/Geometry/Point2D.cs +++ b/Projects/Server/Geometry/Point2D.cs @@ -20,7 +20,7 @@ namespace Server; [Parsable] public struct Point2D : IPoint2D, IComparable, IComparable, IEquatable, IEquatable, - IEquatable + IEquatable, ISpanFormattable { internal int m_X; internal int m_Y; @@ -51,8 +51,6 @@ public struct Point2D { } - public override string ToString() => $"({m_X}, {m_Y})"; - public static Point2D Parse(string value) { var start = value.IndexOfOrdinal('('); @@ -112,4 +110,26 @@ public struct Point2D var xComparison = m_X.CompareTo(other.X); return xComparison != 0 ? xComparison : m_Y.CompareTo(other.Y); } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + return destination.TryWrite(provider, $"({m_X}, {m_Y})", out charsWritten); + } + + public override string ToString() { + // Maximum number of characters that are needed to represent this: + // 4 characters for (, ) + // Up to 11 characters to represent each integer + Span span = stackalloc char[4+11*2]; + this.TryFormat(span, out var charsWritten, null, null); + return span.Slice(0, charsWritten).ToString(); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } + }