From 3707b4758d384048ea962de476b69122e6f9c2ec Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 20 Jul 2024 18:16:01 +0800 Subject: [PATCH] Remove string.Split allocation from parameter append hot path. --- src/Weasel.Core/CommandBuilderBase.cs | 40 +++++ src/Weasel.Core/SpanSplitEnumerator.cs | 227 +++++++++++++++++++++++++ src/Weasel.Postgresql/BatchBuilder.cs | 30 ++++ 3 files changed, 297 insertions(+) create mode 100644 src/Weasel.Core/SpanSplitEnumerator.cs diff --git a/src/Weasel.Core/CommandBuilderBase.cs b/src/Weasel.Core/CommandBuilderBase.cs index bc4a7d3..0796661 100644 --- a/src/Weasel.Core/CommandBuilderBase.cs +++ b/src/Weasel.Core/CommandBuilderBase.cs @@ -302,6 +302,7 @@ public TParameter[] AppendWithParameters(string text) return AppendWithParameters(text, separator); } +#if NET6_0 || NET7_0 /// /// Append a SQL string with user defined placeholder characters for new parameters, and returns an /// array of the newly created parameters @@ -327,6 +328,45 @@ public TParameter[] AppendWithParameters(string text, char separator) return parameters; } +#else + + /// + /// Append a SQL string with user defined placeholder characters for new parameters, and returns an + /// array of the newly created parameters + /// + /// + /// + /// + public TParameter[] AppendWithParameters(string text, char separator) + { + var span = text.AsSpan(); + + var parameters = new TParameter[span.Count(separator)]; + + var enumerator = span.Split(separator); + + var pos = 0; + foreach (var range in enumerator) + { + if (range.Start.Value == 0) + { + // append the first part of the SQL string + _sql.Append(span[range]); + continue; + } + + // Just need a placeholder parameter type and value + var parameter = AddParameter(DBNull.Value, _provider.StringParameterType); + parameters[pos] = parameter; + _sql.Append(_parameterPrefix); + _sql.Append(parameter.ParameterName); + _sql.Append(span[range]); + pos++; + } + + return parameters; + } +#endif } // Note: Those methods are intentionally not written as extension methods diff --git a/src/Weasel.Core/SpanSplitEnumerator.cs b/src/Weasel.Core/SpanSplitEnumerator.cs new file mode 100644 index 0000000..65807ab --- /dev/null +++ b/src/Weasel.Core/SpanSplitEnumerator.cs @@ -0,0 +1,227 @@ +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Weasel.Core; + +// Vendored from .NET 9 Preview. Requires APIs available in .NET 8+. + +#if NET8_0 + +internal static class SpanSplitExtensions +{ + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using the provided separator character. + /// + /// The type of the elements. + /// The source span to be enumerated. + /// The separator character to be used to split the provided span. + /// Returns a . + public static SpanSplitEnumerator Split(this ReadOnlySpan source, T separator) where T : IEquatable => + new SpanSplitEnumerator(source, separator); + + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using the provided separator span. + /// + /// The type of the elements. + /// The source span to be enumerated. + /// The separator span to be used to split the provided span. + /// Returns a . + public static SpanSplitEnumerator Split(this ReadOnlySpan source, ReadOnlySpan separator) + where T : IEquatable => + new SpanSplitEnumerator(source, separator, treatAsSingleSeparator: true); + + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using the provided . + /// + /// The type of the elements. + /// The source span to be enumerated. + /// The to be used to split the provided span. + /// Returns a . + /// + /// Unlike , the is not checked for being empty. + /// An empty will result in no separators being found, regardless of the type of , + /// whereas will use all Unicode whitespace characters as separators if is + /// empty and is . + /// + public static SpanSplitEnumerator SplitAny(this ReadOnlySpan source, SearchValues separators) + where T : IEquatable => + new SpanSplitEnumerator(source, separators); +} + +internal static class SpanSplitConstants +{ + /// A for all of the Unicode whitespace characters + public static readonly SearchValues WhiteSpaceChars = + SearchValues.Create( + "\t\n\v\f\r\u0020\u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000"); +} + +internal ref struct SpanSplitEnumerator where T : IEquatable +{ + /// The input span being split. + private readonly ReadOnlySpan _span; + + /// A single separator to use when is . + private readonly T _separator = default!; + + /// + /// A separator span to use when is (in which case + /// it's treated as a single separator) or (in which case it's treated as a set of separators). + /// + private readonly ReadOnlySpan _separatorBuffer; + + /// A set of separators to use when is . + private readonly SearchValues _searchValues = default!; + + /// Mode that dictates how the instance was configured and how its fields should be used in . + private SpanSplitEnumeratorMode _splitMode; + + /// The inclusive starting index in of the current range. + private int _startCurrent = 0; + + /// The exclusive ending index in of the current range. + private int _endCurrent = 0; + + /// The index in from which the next separator search should start. + private int _startNext = 0; + + /// Gets an enumerator that allows for iteration over the split span. + /// Returns a that can be used to iterate over the split span. + public SpanSplitEnumerator GetEnumerator() => this; + + /// Gets the current element of the enumeration. + /// Returns a instance that indicates the bounds of the current element withing the source span. + public Range Current => new Range(_startCurrent, _endCurrent); + + /// Initializes the enumerator for . + internal SpanSplitEnumerator(ReadOnlySpan span, SearchValues searchValues) + { + _span = span; + _splitMode = SpanSplitEnumeratorMode.SearchValues; + _searchValues = searchValues; + } + + /// Initializes the enumerator for . + /// + /// If is empty and is , as an optimization + /// it will instead use with a cached + /// for all whitespace characters. + /// + internal SpanSplitEnumerator(ReadOnlySpan span, ReadOnlySpan separators) + { + _span = span; + if (typeof(T) == typeof(char) && separators.Length == 0) + { + _searchValues = Unsafe.As>(SpanSplitConstants.WhiteSpaceChars); + _splitMode = SpanSplitEnumeratorMode.SearchValues; + } + else + { + _separatorBuffer = separators; + _splitMode = SpanSplitEnumeratorMode.Any; + } + } + + /// Initializes the enumerator for (or if the separator is empty). + /// must be true. + internal SpanSplitEnumerator(ReadOnlySpan span, ReadOnlySpan separator, bool treatAsSingleSeparator) + { + _span = span; + _separatorBuffer = separator; + _splitMode = separator.Length == 0 ? SpanSplitEnumeratorMode.EmptySequence : SpanSplitEnumeratorMode.Sequence; + } + + /// Initializes the enumerator for . + internal SpanSplitEnumerator(ReadOnlySpan span, T separator) + { + _span = span; + _separator = separator; + _splitMode = SpanSplitEnumeratorMode.SingleElement; + } + + /// + /// Advances the enumerator to the next element of the enumeration. + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the enumeration. + public bool MoveNext() + { + // Search for the next separator index. + int separatorIndex, separatorLength; + switch (_splitMode) + { + case SpanSplitEnumeratorMode.None: + return false; + + case SpanSplitEnumeratorMode.SingleElement: + separatorIndex = _span.Slice(_startNext).IndexOf(_separator); + separatorLength = 1; + break; + + case SpanSplitEnumeratorMode.Any: + separatorIndex = _span.Slice(_startNext).IndexOfAny(_separatorBuffer); + separatorLength = 1; + break; + + case SpanSplitEnumeratorMode.Sequence: + separatorIndex = _span.Slice(_startNext).IndexOf(_separatorBuffer); + separatorLength = _separatorBuffer.Length; + break; + + case SpanSplitEnumeratorMode.EmptySequence: + separatorIndex = -1; + separatorLength = 1; + break; + + default: + separatorIndex = _span.Slice(_startNext).IndexOfAny(_searchValues); + separatorLength = 1; + break; + } + + _startCurrent = _startNext; + if (separatorIndex >= 0) + { + _endCurrent = _startCurrent + separatorIndex; + _startNext = _endCurrent + separatorLength; + } + else + { + _startNext = _endCurrent = _span.Length; + + // Set _splitMode to None so that subsequent MoveNext calls will return false. + _splitMode = SpanSplitEnumeratorMode.None; + } + + return true; + } +} + +/// Indicates in which mode is operating, with regards to how it should interpret its state. +internal enum SpanSplitEnumeratorMode +{ + /// Either a default was used, or the enumerator has finished enumerating and there's no more work to do. + None = 0, + + /// A single T separator was provided. + SingleElement, + + /// A span of separators was provided, each of which should be treated independently. + Any, + + /// The separator is a span of elements to be treated as a single sequence. + Sequence, + + /// The separator is an empty sequence, such that no splits should be performed. + EmptySequence, + + /// + /// A was provided and should behave the same as with but with the separators in the + /// instance instead of in a . + /// + SearchValues +} + +#endif diff --git a/src/Weasel.Postgresql/BatchBuilder.cs b/src/Weasel.Postgresql/BatchBuilder.cs index 04821f4..ae443bf 100644 --- a/src/Weasel.Postgresql/BatchBuilder.cs +++ b/src/Weasel.Postgresql/BatchBuilder.cs @@ -1,6 +1,7 @@ using System.Text; using Npgsql; using NpgsqlTypes; +using Weasel.Core; namespace Weasel.Postgresql; @@ -108,6 +109,7 @@ public NpgsqlParameter[] AppendWithParameters(string text) return AppendWithParameters(text, '?'); } +#if NET6_0 || NET7_0 public NpgsqlParameter[] AppendWithParameters(string text, char placeholder) { var split = text.Split(placeholder); @@ -124,7 +126,35 @@ public NpgsqlParameter[] AppendWithParameters(string text, char placeholder) return parameters; } +#else + public NpgsqlParameter[] AppendWithParameters(string text, char separator) + { + var span = text.AsSpan(); + + var parameters = new NpgsqlParameter[span.Count(separator)]; + + var enumerator = span.Split(separator); + var pos = 0; + foreach (var range in enumerator) + { + if (range.Start.Value == 0) + { + // append the first part of the SQL string + _builder.Append(span[range]); + continue; + } + + // Just need a placeholder parameter type and value + var parameter = AppendParameter(DBNull.Value, NpgsqlDbType.Text); + parameters[pos] = parameter; + _builder.Append(span[range]); + pos++; + } + + return parameters; + } +#endif public void StartNewCommand() { if (_current != null)