From 67a7f589c3f7f28ada0fdbf0cc2efe9012d83d00 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:50:04 -0500 Subject: [PATCH] Improved ToString() performance. (#8) --- src/LightResults/Common/ResultBase.cs | 57 +++++-- src/LightResults/Error.cs | 15 +- src/LightResults/Result.cs | 139 ++++++++++------ .../Benchmarks.cs | 151 +++++++++++++++--- .../Benchmarks.cs | 128 +++++++++++++-- 5 files changed, 380 insertions(+), 110 deletions(-) diff --git a/src/LightResults/Common/ResultBase.cs b/src/LightResults/Common/ResultBase.cs index 8e45547..120bee4 100644 --- a/src/LightResults/Common/ResultBase.cs +++ b/src/LightResults/Common/ResultBase.cs @@ -1,6 +1,4 @@ -using System.Text; - -namespace LightResults.Common; +namespace LightResults.Common; /// Base class for implementing the interface. public abstract class ResultBase : IResult @@ -54,21 +52,46 @@ public bool HasError() where TError : IError /// public override string ToString() { - var builder = new StringBuilder(); - builder.Append(GetType().Name); - builder.Append(" { "); - builder.Append("IsSuccess = "); - builder.Append(IsSuccess); + var typeName = GetType().Name; + if (IsSuccess) + return GetResultString(typeName, "True", ""); - if (IsFailed && Errors[0].Message.Length > 0) - { - builder.Append(", Error = "); - builder.Append('"'); - builder.Append(Errors[0].Message); - builder.Append('"'); - } + var errorString = GetErrorString(); + return GetResultString(typeName, "False", errorString); + } + + internal static string GetResultString(string typeName, string successString, string informationString) + { + const string preResultStr = " { IsSuccess = "; + const string postResultStr = " }"; +#if NET6_0_OR_GREATER + var stringLength = typeName.Length + preResultStr.Length + successString.Length + informationString.Length + postResultStr.Length; + + var str = string.Create(stringLength, (typeName, successString, informationString), (span, state) => { span.TryWrite($"{state.typeName}{preResultStr}{state.successString}{state.informationString}{postResultStr}", out _); }); + + return str; +#else + return $"{typeName}{preResultStr}{successString}{informationString}{postResultStr}"; +#endif + } + + internal string GetErrorString() + { + if (IsSuccess || Errors[0].Message.Length <= 0) + return ""; + + var errorMessage = Errors[0].Message; + + const string preErrorStr = ", Error = \""; + const string postErrorStr = "\""; +#if NET6_0_OR_GREATER + var stringLength = preErrorStr.Length + errorMessage.Length + postErrorStr.Length; + + var str = string.Create(stringLength, errorMessage, (span, state) => { span.TryWrite($"{preErrorStr}{state}{postErrorStr}", out _); }); - builder.Append(" }"); - return builder.ToString(); + return str; +#else + return $"{preErrorStr}{errorMessage}{postErrorStr}"; +#endif } } diff --git a/src/LightResults/Error.cs b/src/LightResults/Error.cs index 73e461e..bf879d1 100644 --- a/src/LightResults/Error.cs +++ b/src/LightResults/Error.cs @@ -11,7 +11,7 @@ namespace LightResults; public class Error : IError { internal static Error Empty { get; } = new(); - + /// public string Message { get; } @@ -41,15 +41,10 @@ public Error(string message) #endif } - /// Initializes a new instance of the class with the specified metadata. - /// The metadata associated with the error. - public Error((string Key, object Value) metadata) : this("", metadata) - { - } /// Initializes a new instance of the class with the specified metadata. /// The metadata associated with the error. - public Error(IDictionary metadata) : this("", metadata) + public Error((string Key, object Value) metadata) : this("", metadata) { } @@ -69,6 +64,12 @@ public Error(string message, (string Key, object Value) metadata) #endif } + /// Initializes a new instance of the class with the specified metadata. + /// The metadata associated with the error. + public Error(IDictionary metadata) : this("", metadata) + { + } + /// Initializes a new instance of the class with the specified error message and metadata. /// The error message. /// The metadata associated with the error. diff --git a/src/LightResults/Result.cs b/src/LightResults/Result.cs index c530179..4b6c939 100644 --- a/src/LightResults/Result.cs +++ b/src/LightResults/Result.cs @@ -1,4 +1,3 @@ -using System.Text; using LightResults.Common; namespace LightResults; @@ -10,8 +9,8 @@ public sealed class Result : ResultBase #endif { private static readonly Result OkResult = new(); - private static readonly Result FailResult = new(Error.Empty); - + private static readonly Result FailedResult = new(Error.Empty); + private Result() { } @@ -44,7 +43,7 @@ public static Result Ok(TValue value) /// A new instance of representing a failed result. public static Result Fail() { - return FailResult; + return FailedResult; } /// Creates a failed result with the given error message. @@ -52,7 +51,8 @@ public static Result Fail() /// A new instance of representing a failed result with the specified error message. public static Result Fail(string errorMessage) { - return new Result(new Error(errorMessage)); + var error = new Error(errorMessage); + return Fail(error); } /// Creates a failed result with the given error message and metadata. @@ -61,7 +61,8 @@ public static Result Fail(string errorMessage) /// A new instance of representing a failed result with the specified error message and metadata. public static Result Fail(string errorMessage, (string Key, object Value) metadata) { - return new Result(new Error(errorMessage, metadata)); + var error = new Error(errorMessage, metadata); + return Fail(error); } /// Creates a failed result with the given error message and metadata. @@ -70,7 +71,8 @@ public static Result Fail(string errorMessage, (string Key, object Value) metada /// A new instance of representing a failed result with the specified error message and metadata. public static Result Fail(string errorMessage, IDictionary metadata) { - return new Result(new Error(errorMessage, metadata)); + var error = new Error(errorMessage, metadata); + return Fail(error); } /// Creates a failed result with the given error. @@ -143,6 +145,19 @@ public static Result Fail(IEnumerable errors) { return Result.Fail(errors); } + + /// + public override string ToString() + { + if (IsSuccess) + return $"{nameof(Result)} {{ IsSuccess = True }}"; + + if (Errors[0].Message.Length == 0) + return $"{nameof(Result)} {{ IsSuccess = False }}"; + + var errorString = GetErrorString(); + return GetResultString(nameof(Result), "False", errorString); + } } /// Represents a result. @@ -154,7 +169,7 @@ public sealed class Result : ResultBase , IResult #endif { - private static readonly Result FailResult = new(Error.Empty); + private static readonly Result FailedResult = new(Error.Empty); /// Gets the value of the result, throwing an exception if the result is failed. /// Thrown when attempting to get or set the value of a failed result. @@ -210,7 +225,7 @@ public static Result Ok(TValue value) /// A new instance of representing a failed result. public static Result Fail() { - return FailResult; + return FailedResult; } /// Creates a failed result with the given error message. @@ -218,7 +233,8 @@ public static Result Fail() /// A new instance of representing a failed result with the specified error message. public static Result Fail(string errorMessage) { - return new Result(new Error(errorMessage)); + var error = new Error(errorMessage); + return Fail(error); } /// Creates a failed result with the given error message and metadata. @@ -227,7 +243,8 @@ public static Result Fail(string errorMessage) /// A new instance of representing a failed result with the specified error message. public static Result Fail(string errorMessage, (string Key, object Value) metadata) { - return new Result(new Error(errorMessage, metadata)); + var error = new Error(errorMessage, metadata); + return Fail(error); } /// Creates a failed result with the given error message and metadata. @@ -236,7 +253,8 @@ public static Result Fail(string errorMessage, (string Key, object Value /// A new instance of representing a failed result with the specified error message. public static Result Fail(string errorMessage, IDictionary metadata) { - return new Result(new Error(errorMessage, metadata)); + var error = new Error(errorMessage, metadata); + return Fail(error); } /// Creates a failed result with the given error. @@ -258,50 +276,73 @@ public static Result Fail(IEnumerable errors) /// public override string ToString() { - var builder = new StringBuilder(); - builder.Append(nameof(Result)); - builder.Append(" { "); - builder.Append("IsSuccess = "); - builder.Append(IsSuccess); - if (IsSuccess) { - if (Value is bool || Value is sbyte || Value is byte || Value is short || Value is ushort || Value is int || Value is uint || Value is long || Value is ulong || + var valueString = GetValueString(); + return GetResultString(nameof(Result), "True", valueString); + } + + if (Errors[0].Message.Length == 0) + return $"{nameof(Result)} {{ IsSuccess = False }}"; + + var errorString = GetErrorString(); + return GetResultString(nameof(Result), "False", errorString); + } + + private string GetValueString() + { + if (IsFailed) + return ""; + + var valueString = Value?.ToString() ?? ""; + + const string preValueStr = ", Value = "; + const string charStr = "'"; + const string stringStr = "\""; + + if (Value is bool || Value is sbyte || Value is byte || Value is short || Value is ushort || Value is int || Value is uint || Value is long || Value is ulong || #if NET7_0_OR_GREATER - Value is Int128 || Value is UInt128 || + Value is Int128 || Value is UInt128 || +#endif + Value is decimal || Value is float || Value is double) + { +#if NET6_0_OR_GREATER + var stringLength = preValueStr.Length + valueString.Length; + + var str = string.Create(stringLength, valueString, (span, state) => { span.TryWrite($"{preValueStr}{state}", out _); }); + + return str; +#else + return $"{preValueStr}{valueString}"; #endif - Value is decimal || Value is float || Value is double) - { - builder.Append(", Value = "); - builder.Append(Value); - } - - if (Value is char) - { - builder.Append(", Value = "); - builder.Append('\''); - builder.Append(Value); - builder.Append('\''); - } - - if (Value is string) - { - builder.Append(", Value = "); - builder.Append('"'); - builder.Append(Value); - builder.Append('"'); - } } - if (IsFailed && Errors[0].Message.Length > 0) + if (Value is char) { - builder.Append(", Error = "); - builder.Append('"'); - builder.Append(Errors[0].Message); - builder.Append('"'); +#if NET6_0_OR_GREATER + var stringLength = preValueStr.Length + charStr.Length + valueString.Length + charStr.Length; + + var str = string.Create(stringLength, valueString, (span, state) => { span.TryWrite($"{preValueStr}{charStr}{state}{charStr}", out _); }); + + return str; +#else + return $"{preValueStr}{charStr}{valueString}{charStr}"; +#endif } - builder.Append(" }"); - return builder.ToString(); + if (Value is string) + { +#if NET6_0_OR_GREATER + var stringLength = preValueStr.Length + stringStr.Length + valueString.Length + stringStr.Length; + + var str = string.Create(stringLength, valueString, (span, state) => { span.TryWrite($"{preValueStr}{stringStr}{state}{stringStr}", out _); }); + + return str; +#else + return $"{preValueStr}{stringStr}{valueString}{stringStr}"; +#endif + } + + return ""; } -} +} \ No newline at end of file diff --git a/tools/LightResults.CurrentBenchmarks/Benchmarks.cs b/tools/LightResults.CurrentBenchmarks/Benchmarks.cs index ad5ec21..864ef40 100644 --- a/tools/LightResults.CurrentBenchmarks/Benchmarks.cs +++ b/tools/LightResults.CurrentBenchmarks/Benchmarks.cs @@ -1,76 +1,153 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Jobs; +using LightResults.Common; namespace LightResults.CurrentBenchmarks; [MemoryDiagnoser] +[SimpleJob(RuntimeMoniker.Net80)] +[HideColumns(Column.Job, Column.Iterations, Column.Error, Column.StdDev, Column.Gen0, Column.Gen1, Column.Gen2)] public class Benchmarks { - private const int ResultValue = 0; - private const string ErrorMessage = "An unknown error occured."; - private static readonly Error Error = new(ErrorMessage); - private static readonly Result FailedResult = Result.Fail(Error); - [Params(100_000)] public int Iterations { get; set; } + private const int ResultValue = 0; + private const string ErrorMessage = "An unknown error occured."; + private static readonly Error ErrorWithMessage = new(ErrorMessage); + private static readonly Result ResultOk = Result.Ok(); + private static readonly Result ResultFail = Result.Fail(); + private static readonly Result ResultFailWithMessage = Result.Fail(ErrorWithMessage); + private static readonly Result ResultTValueOk = Result.Ok(ResultValue); + private static readonly Result ResultTValueFail = Result.Fail(); + private static readonly Result ResultTValueFailWithMessage = Result.Fail(ErrorWithMessage); + private static readonly CustomResult CustomResultOk = CustomResult.Ok(); + private static readonly CustomResult CustomResultFail = CustomResult.Fail(); + private static readonly CustomResult CustomResultFailWithMessage = CustomResult.Fail(ErrorWithMessage); + + [Benchmark] + public void Develop_ResultBaseIndexer() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFailWithMessage.Errors[0]; + } + + [Benchmark] + public void Develop_ResultBaseHasError() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFailWithMessage.HasError(); + } + + [Benchmark] + public void Develop_ResultOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultOk.ToString(); + } + + [Benchmark] + public void Develop_ResultFailToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFail.ToString(); + } + + [Benchmark] + public void Develop_ResultFailWithMessageToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFailWithMessage.ToString(); + } + + [Benchmark] + public void Develop_ResultTValueOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultTValueOk.ToString(); + } + [Benchmark] - public void Current_ResultBaseIndexer() + public void Develop_ResultTValueFailToString() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = FailedResult.Errors[0]; + _ = ResultTValueFail.ToString(); } + [Benchmark] - public void Current_ResultBaseHasError() + public void Develop_ResultTValueFailWithMessageToString() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = FailedResult.HasError(); + _ = ResultTValueFailWithMessage.ToString(); } [Benchmark] - public void Current_ResultOk() + public void Develop_CustomResultOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultOk.ToString(); + } + + [Benchmark] + public void Develop_CustomResultFailToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultFail.ToString(); + } + + [Benchmark] + public void Develop_CustomResultFailWithMessageToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultFailWithMessage.ToString(); + } + + [Benchmark] + public void Develop_ResultOk() { for (var iteration = 0; iteration < Iterations; iteration++) _ = Result.Ok(); } [Benchmark] - public void Current_ResultFail() + public void Develop_ResultFail() { for (var iteration = 0; iteration < Iterations; iteration++) _ = Result.Fail(); } [Benchmark] - public void Current_ResultFailWithError() + public void Develop_ResultFailWithError() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] - public void Current_ResultTValueOk() + public void Develop_ResultTValueOk() { for (var iteration = 0; iteration < Iterations; iteration++) _ = Result.Ok(ResultValue); } [Benchmark] - public void Current_ResultTValueFail() + public void Develop_ResultTValueFail() { for (var iteration = 0; iteration < Iterations; iteration++) _ = Result.Fail(); } [Benchmark] - public void Current_ResultTValueFailWithError() + public void Develop_ResultTValueFailWithError() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] - public void Current_ResultOkTValue() + public void Develop_ResultOkTValue() { // ReSharper disable once RedundantTypeArgumentsOfMethod for (var iteration = 0; iteration < Iterations; iteration++) @@ -78,30 +155,56 @@ public void Current_ResultOkTValue() } [Benchmark] - public void Current_ResultFailTValue() + public void Develop_ResultFailTValue() { for (var iteration = 0; iteration < Iterations; iteration++) _ = Result.Fail(); } [Benchmark] - public void Current_ResultFailWithErrorTValue() + public void Develop_ResultFailWithErrorTValue() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] - public void Current_NewError() + public void Develop_NewError() { for (var iteration = 0; iteration < Iterations; iteration++) _ = new Error(); } [Benchmark] - public void Current_NewErrorWithString() + public void Develop_NewErrorWithString() { for (var iteration = 0; iteration < Iterations; iteration++) _ = new Error(ErrorMessage); } -} + + private sealed class CustomResult : ResultBase + { + private CustomResult() + { + } + + private CustomResult(IError error) : base(error) + { + } + + public static CustomResult Ok() + { + return new CustomResult(); + } + + public static CustomResult Fail() + { + return new CustomResult(new Error()); + } + + public static CustomResult Fail(IError error) + { + return new CustomResult(error); + } + } +} \ No newline at end of file diff --git a/tools/LightResults.DevelopBenchmarks/Benchmarks.cs b/tools/LightResults.DevelopBenchmarks/Benchmarks.cs index f90266e..b2495b1 100644 --- a/tools/LightResults.DevelopBenchmarks/Benchmarks.cs +++ b/tools/LightResults.DevelopBenchmarks/Benchmarks.cs @@ -1,31 +1,107 @@ -using System.Collections.Immutable; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Jobs; +using LightResults.Common; namespace LightResults.DevelopBenchmarks; [MemoryDiagnoser] +[SimpleJob(RuntimeMoniker.Net80)] +[HideColumns(Column.Job, Column.Iterations, Column.Error, Column.StdDev, Column.Gen0, Column.Gen1, Column.Gen2)] public class Benchmarks { - private const int ResultValue = 0; - private const string ErrorMessage = "An unknown error occured."; - private static readonly Error Error = new(ErrorMessage); - private static readonly Result FailedResult = Result.Fail(Error); - [Params(100_000)] public int Iterations { get; set; } + private const int ResultValue = 0; + private const string ErrorMessage = "An unknown error occured."; + private static readonly Error ErrorWithMessage = new(ErrorMessage); + private static readonly Result ResultOk = Result.Ok(); + private static readonly Result ResultFail = Result.Fail(); + private static readonly Result ResultFailWithMessage = Result.Fail(ErrorWithMessage); + private static readonly Result ResultTValueOk = Result.Ok(ResultValue); + private static readonly Result ResultTValueFail = Result.Fail(); + private static readonly Result ResultTValueFailWithMessage = Result.Fail(ErrorWithMessage); + private static readonly CustomResult CustomResultOk = CustomResult.Ok(); + private static readonly CustomResult CustomResultFail = CustomResult.Fail(); + private static readonly CustomResult CustomResultFailWithMessage = CustomResult.Fail(ErrorWithMessage); + [Benchmark] public void Develop_ResultBaseIndexer() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = FailedResult.Errors[0]; + _ = ResultFailWithMessage.Errors[0]; } [Benchmark] public void Develop_ResultBaseHasError() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = FailedResult.HasError(); + _ = ResultFailWithMessage.HasError(); + } + + [Benchmark] + public void Develop_ResultOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultOk.ToString(); + } + + [Benchmark] + public void Develop_ResultFailToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFail.ToString(); + } + + [Benchmark] + public void Develop_ResultFailWithMessageToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultFailWithMessage.ToString(); + } + + [Benchmark] + public void Develop_ResultTValueOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultTValueOk.ToString(); + } + + [Benchmark] + public void Develop_ResultTValueFailToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultTValueFail.ToString(); + } + + + [Benchmark] + public void Develop_ResultTValueFailWithMessageToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = ResultTValueFailWithMessage.ToString(); + } + + [Benchmark] + public void Develop_CustomResultOkToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultOk.ToString(); + } + + [Benchmark] + public void Develop_CustomResultFailToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultFail.ToString(); + } + + [Benchmark] + public void Develop_CustomResultFailWithMessageToString() + { + for (var iteration = 0; iteration < Iterations; iteration++) + _ = CustomResultFailWithMessage.ToString(); } [Benchmark] @@ -46,7 +122,7 @@ public void Develop_ResultFail() public void Develop_ResultFailWithError() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] @@ -67,7 +143,7 @@ public void Develop_ResultTValueFail() public void Develop_ResultTValueFailWithError() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] @@ -89,7 +165,7 @@ public void Develop_ResultFailTValue() public void Develop_ResultFailWithErrorTValue() { for (var iteration = 0; iteration < Iterations; iteration++) - _ = Result.Fail(Error); + _ = Result.Fail(ErrorWithMessage); } [Benchmark] @@ -105,4 +181,30 @@ public void Develop_NewErrorWithString() for (var iteration = 0; iteration < Iterations; iteration++) _ = new Error(ErrorMessage); } -} + + private sealed class CustomResult : ResultBase + { + private CustomResult() + { + } + + private CustomResult(IError error) : base(error) + { + } + + public static CustomResult Ok() + { + return new CustomResult(); + } + + public static CustomResult Fail() + { + return new CustomResult(new Error()); + } + + public static CustomResult Fail(IError error) + { + return new CustomResult(error); + } + } +} \ No newline at end of file