diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 6fe3ae3d..6b1893a7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -35,7 +35,6 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 7.0.x 8.0.x - name: Setup .NET (global.json) uses: actions/setup-dotnet@v4 @@ -78,7 +77,6 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - configuration: [Debug, Release] runs-on: ${{ matrix.os }} @@ -88,7 +86,6 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 7.0.x 8.0.x - name: Setup .NET (global.json) uses: actions/setup-dotnet@v4 @@ -96,8 +93,6 @@ jobs: global-json-file: global.json - name: Restore dependencies run: dotnet restore - - name: Build - run: dotnet build -c ${{ matrix.configuration }} --no-restore - name: Test Parsers shell: pwsh run: ./test-parsers.ps1 @@ -116,7 +111,6 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 7.0.x 8.0.x - name: Setup .NET (global.json) uses: actions/setup-dotnet@v4 diff --git a/README.md b/README.md index 619dbc0b..dbebcb33 100644 --- a/README.md +++ b/README.md @@ -2130,8 +2130,12 @@ namespace nietras.SeparatedValues public void Set(System.IFormatProvider? provider, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument(new string?[]?[] { "", "provider"})] ref nietras.SeparatedValues.SepWriter.Col.FormatInterpolatedStringHandler handler) { } + [System.Obsolete(("Types with embedded references are not supported in this version of your compiler" + + "."), true)] + [System.Runtime.CompilerServices.CompilerFeatureRequired("RefStructs")] [System.Runtime.CompilerServices.InterpolatedStringHandler] - public readonly struct FormatInterpolatedStringHandler + [System.Runtime.CompilerServices.IsByRefLike] + public struct FormatInterpolatedStringHandler { public FormatInterpolatedStringHandler(int literalLength, int formattedCount, nietras.SeparatedValues.SepWriter.Col col) { } public FormatInterpolatedStringHandler(int literalLength, int formattedCount, nietras.SeparatedValues.SepWriter.Col col, System.IFormatProvider? provider) { } diff --git a/src/Sep.Benchmarks/Program.cs b/src/Sep.Benchmarks/Program.cs index 32c1e613..d4dd8c70 100644 --- a/src/Sep.Benchmarks/Program.cs +++ b/src/Sep.Benchmarks/Program.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Threading; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; @@ -22,16 +23,16 @@ { var config = (Debugger.IsAttached ? new DebugInProcessConfig() : DefaultConfig.Instance) .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(200)) - .AddColumn(MBPerSecFromCharsLength()) + //.AddColumn(MBPerSecFromCharsLength()) ; - //BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args, config); + BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args, config); //BenchmarkRunner.Run(typeof(SepReaderBench), config, args); //BenchmarkRunner.Run(typeof(SepWriterBench), config, args); //BenchmarkRunner.Run(typeof(SepReaderWriterBench), config, args); //BenchmarkRunner.Run(typeof(SepEndToEndBench), config, args); //BenchmarkRunner.Run(typeof(SepHashBench), config, args); //BenchmarkRunner.Run(typeof(SepParseSeparatorsMaskBench), config, args); - BenchmarkRunner.Run(typeof(SepParserBench), config, args); + //BenchmarkRunner.Run(typeof(SepParserBench), config, args); //BenchmarkRunner.Run(typeof(StopwatchBench), config, args); } else @@ -46,8 +47,10 @@ } } +#pragma warning disable CS8321 // Local function is declared but never used static IColumn MBPerSecFromCharsLength() => new BytesStatisticColumn("MB/s", BytesFromCharsLength, BytesStatisticColumn.FormatMBPerSec); +#pragma warning restore CS8321 // Local function is declared but never used static long BytesFromCharsLength(IReadOnlyList parameters) { diff --git a/src/Sep.Benchmarks/SepWriterBench.cs b/src/Sep.Benchmarks/SepWriterBench.cs index 3f649e14..f2fc46a8 100644 --- a/src/Sep.Benchmarks/SepWriterBench.cs +++ b/src/Sep.Benchmarks/SepWriterBench.cs @@ -11,7 +11,7 @@ public class SepWriterBench [IterationSetup] public void Setup() { - _writer = Sep.Writer().ToText(256 * 1024 * 1024); + _writer = Sep.Writer(o => o with { WriteHeader = false }).ToText(256 * 1024 * 1024); } [IterationCleanup] @@ -36,4 +36,14 @@ public void SetByColName() writeRow["C"].Set("cccccccccccccccccccccccc"); writeRow["D"].Set("ddddddddddddddddddddddddddddddd"); } + + [Benchmark] + public void SetByColIndex() + { + using var writeRow = _writer!.NewRow(); + writeRow[0].Set("aaaaaaaaaaaaaaaaaaa"); + writeRow[1].Set("bbbbbbbbbbbbbbbbbbbbbb"); + writeRow[2].Set("cccccccccccccccccccccccc"); + writeRow[3].Set("ddddddddddddddddddddddddddddddd"); + } } diff --git a/src/Sep.Test/Internals/InterpolatedStringHandlerTest.cs b/src/Sep.Test/Internals/InterpolatedStringHandlerTest.cs index de4ab5c5..9078ce10 100644 --- a/src/Sep.Test/Internals/InterpolatedStringHandlerTest.cs +++ b/src/Sep.Test/Internals/InterpolatedStringHandlerTest.cs @@ -10,6 +10,27 @@ namespace nietras.SeparatedValues.Test.Internals; [TestClass] public class InterpolatedStringHandlerTest { +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_pos")] + static extern ref int Position(ref DefaultInterpolatedStringHandler handler); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_arrayToReturnToPool")] + static extern ref char[]? ArrayToReturnToPool(ref DefaultInterpolatedStringHandler handler); + + [TestMethod] + public void InterpolatedStringHandlerTest_DefaultInterpolatedStringHandler_Accessor() + { + var buffer = new char[16]; + var handler = new DefaultInterpolatedStringHandler(literalLength: 10, formattedCount: 2, provider: null, buffer); + ref var position = ref Position(ref handler); + position = 2; + handler.AppendFormatted(42); + var text = handler.ToStringAndClear(); + Assert.IsNotNull(text); + } +#endif + [TestMethod] public void InterpolatedStringHandlerTest_Log() { diff --git a/src/Sep.Test/SepWriterColTest.cs b/src/Sep.Test/SepWriterColTest.cs index d38fa942..40673d52 100644 --- a/src/Sep.Test/SepWriterColTest.cs +++ b/src/Sep.Test/SepWriterColTest.cs @@ -10,6 +10,7 @@ public class SepWriterColTest const string ColName = "A"; const int ColValue = 123456; const string ColText = "123456"; + static readonly string ColTextLong = new('a', 2048); static readonly string NL = Environment.NewLine; @@ -31,12 +32,24 @@ public void SepWriterColTest_Set_String() Run(col => col.Set(ColText)); } + [TestMethod] + public void SepWriterColTest_Set_String_Long() + { + Run(col => col.Set(ColTextLong), ColTextLong); + } + [TestMethod] public void SepWriterColTest_Set_Span() { Run(col => col.Set(ColText.AsSpan())); } + [TestMethod] + public void SepWriterColTest_Set_Span_Long() + { + Run(col => col.Set(ColTextLong.AsSpan()), ColTextLong); + } + [TestMethod] public void SepWriterColTest_Set_InterpolatedString() { @@ -61,6 +74,18 @@ public void SepWriterColTest_Set_InterpolatedString_F2_CultureInfoAsParam() Run(col => col.Set(CultureInfo.GetCultureInfo("da-DK"), $"{ColValue:F2}"), ColText + ",00"); } + [TestMethod] + public void SepWriterColTest_Set_InterpolatedString_F2_CultureInfoAsConfig_Null() + { + Run(col => col.Set($"{ColValue:F2}"), ColText + ".00", null); + } + + [TestMethod] + public void SepWriterColTest_Set_InterpolatedString_F2_CultureInfoAsParam_Null() + { + Run(col => col.Set(provider: null, $"{ColValue:F2}"), ColText + ".00"); + } + [TestMethod] public void SepWriterColTest_Set_InterpolatedString_AppendLiteral() { @@ -111,6 +136,26 @@ public void SepWriterColTest_Format() Run(col => col.Format(ColValue)); } + [TestMethod] + public void SepWriterColTest_Format_Long() + { + var f = new LongSpanFormattable(); + Run(col => col.Format(f), f.Text); + } + + public class LongSpanFormattable : ISpanFormattable + { + public string Text { get; } = ColTextLong; + + public string ToString(string? format, IFormatProvider? formatProvider) => Text; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + charsWritten = Text.Length; + return Text.TryCopyTo(destination); + } + } + // No escaping needed [DataRow("", "")] [DataRow(" ", " ")] diff --git a/src/Sep/SepWriter.Col.cs b/src/Sep/SepWriter.Col.cs index d120a3d1..6759de2c 100644 --- a/src/Sep/SepWriter.Col.cs +++ b/src/Sep/SepWriter.Col.cs @@ -1,30 +1,60 @@ using System; +using System.Buffers; using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Text; namespace nietras.SeparatedValues; public partial class SepWriter { - internal sealed class ColImpl + internal sealed class ColImpl(SepWriter writer, int index, string name) { - internal readonly SepWriter _writer; +#if DEBUG + internal const int MinimumLength = 4; +#else + internal const int MinimumLength = 256; +#endif + internal readonly SepWriter _writer = writer; + internal char[] _buffer = ArrayPool.Shared.Rent(MinimumLength); + internal int _position = 0; + + public int Index { get; } = index; + public string Name { get; } = name; + public bool HasBeenSet { get; set; } = false; + + public void Clear() { HasBeenSet = false; _position = 0; } - public ColImpl(SepWriter writer, int index, string name, StringBuilder text) + public void Append(ReadOnlySpan value) { - _writer = writer; - Index = index; - Name = name; - Text = text; + EnsureCapacity(value.Length); + value.CopyTo(_buffer.AsSpan(_position)); + _position += value.Length; } - public int Index { get; private set; } - public string Name { get; } - public StringBuilder Text { get; } - public bool HasBeenSet { get; set; } = false; + public ReadOnlySpan GetSpan() => _buffer.AsSpan(0, _position); + + public void Dispose() + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + + void EnsureCapacity(int additionalLength) + { + if (_position + additionalLength > _buffer.Length) + { + GrowBuffer(additionalLength); + } + } - public void Clear() { HasBeenSet = false; Text.Clear(); } + void GrowBuffer(int additionalLength) + { + var newSize = Math.Max(_buffer.Length * 2, _position + additionalLength); + var newBuffer = ArrayPool.Shared.Rent(newSize); + _buffer.AsSpan(0, _position).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } } #pragma warning disable CA1815 // Override equals and operator equals on value types @@ -33,36 +63,28 @@ public readonly ref struct Col { readonly ColImpl _impl; - internal Col(ColImpl impl) - { - _impl = impl; - } + internal Col(ColImpl impl) => _impl = impl; internal int ColIndex => _impl.Index; internal string ColName => _impl.Name; #pragma warning disable CA1822 // Mark members as static -#pragma warning disable IDE0060 // Remove unused parameter #pragma warning disable CA1045 // Do not pass types by reference public void Set([InterpolatedStringHandlerArgument("")] ref FormatInterpolatedStringHandler handler) -#pragma warning restore CA1045 // Do not pass types by reference -#pragma warning restore IDE0060 // Remove unused parameter -#pragma warning restore CA1822 // Mark members as static - { } - -#pragma warning disable CA1822 // Mark members as static -#pragma warning disable IDE0060 // Remove unused parameter + { + handler.Finish(); + } public void Set(IFormatProvider? provider, -#pragma warning disable CA1045 // Do not pass types by reference - [InterpolatedStringHandlerArgument("", "provider")] ref FormatInterpolatedStringHandler handler) + [InterpolatedStringHandlerArgument("", nameof(provider))] ref FormatInterpolatedStringHandler handler) + { + handler.Finish(); + } #pragma warning restore CA1045 // Do not pass types by reference -#pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore CA1822 // Mark members as static - { } public void Set(ReadOnlySpan span) { - var text = _impl.Text; + var text = _impl; text.Clear(); text.Append(span); MarkSet(); @@ -71,37 +93,52 @@ public void Set(ReadOnlySpan span) public void Format(T value) where T : ISpanFormattable { var impl = _impl; - var text = impl.Text; - text.Clear(); - var handler = new StringBuilder.AppendInterpolatedStringHandler(0, 1, text, impl._writer._cultureInfo); - handler.AppendFormatted(value); + impl.Clear(); + if (value.TryFormat(impl._buffer, out var charsWritten, null, impl._writer._cultureInfo)) + { + impl._position = charsWritten; + } + else + { + var handler = new FormatInterpolatedStringHandler(0, 1, this); + handler.AppendFormatted(value); + handler.Finish(); + } MarkSet(); } - /// Provides a handler used by the language compiler to append interpolated strings into instances. + /// + /// Provides a handler used by the language compiler to append + /// interpolated strings into instances. + /// [EditorBrowsable(EditorBrowsableState.Never)] [InterpolatedStringHandler] -#pragma warning disable CA1815 // Override equals and operator equals on value types - public readonly struct FormatInterpolatedStringHandler -#pragma warning restore CA1815 // Override equals and operator equals on value types + public ref struct FormatInterpolatedStringHandler { readonly ColImpl _impl; - readonly StringBuilder.AppendInterpolatedStringHandler _handler; + DefaultInterpolatedStringHandler _handler; - public FormatInterpolatedStringHandler(int literalLength, int formattedCount, Col col) + public FormatInterpolatedStringHandler(int literalLength, int formattedCount, + Col col) { _impl = col._impl; - var text = _impl.Text; - text.Clear(); - _handler = new(literalLength, formattedCount, text, _impl._writer._cultureInfo); + _impl.Clear(); + _handler = new(literalLength, formattedCount, + _impl._writer._cultureInfo, _impl._buffer); + Position(ref _handler) = _impl._position; + ArrayToReturnToPool(ref _handler) = _impl._buffer; } - public FormatInterpolatedStringHandler(int literalLength, int formattedCount, Col col, IFormatProvider? provider) + + public FormatInterpolatedStringHandler(int literalLength, int formattedCount, + Col col, IFormatProvider? provider) { _impl = col._impl; - var text = _impl.Text; - text.Clear(); - _handler = new(literalLength, formattedCount, text, provider); + _impl.Clear(); + _handler = new(literalLength, formattedCount, + provider ?? _impl._writer._cultureInfo, _impl._buffer); + Position(ref _handler) = _impl._position; + ArrayToReturnToPool(ref _handler) = _impl._buffer; } public void AppendLiteral(string value) @@ -165,6 +202,33 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n } void MarkSet() => _impl.HasBeenSet = true; + + internal void Finish() + { + ref var handlerArrayRef = ref ArrayToReturnToPool(ref _handler); + A.Assert(handlerArrayRef is not null); + _impl._buffer = handlerArrayRef!; + _impl._position = Position(ref _handler); + handlerArrayRef = null; + // Do not call *Clear() on handler as Col takes ownership of + // array from ArrayPool. + } + + // Avoid recreating DefaultInterpolatedStringHandler while being + // able to reuse array from ArrayPool by using UnsafeAccessor to + // access internal state of this. This works fine for net8.0 and + // net9.0 but there are no guarantees if this could change in the + // future, if so consider using #if NET10_0_OR_GREATER or similar to + // address any changes or consider then copying the entire + // DefaultInterpolatedStringHandler source code and adopt for needs. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_arrayToReturnToPool")] + static extern ref char[]? ArrayToReturnToPool(ref DefaultInterpolatedStringHandler handler); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_pos")] + static extern ref int Position(ref DefaultInterpolatedStringHandler handler); } void MarkSet() => _impl.HasBeenSet = true; diff --git a/src/Sep/SepWriter.Row.cs b/src/Sep/SepWriter.Row.cs index 5c65d27e..2ce7c0ee 100644 --- a/src/Sep/SepWriter.Row.cs +++ b/src/Sep/SepWriter.Row.cs @@ -91,7 +91,7 @@ internal ColImpl GetOrAddCol(int colIndex) var cols = _cols; if (colIndex == cols.Count && (!_writeHeader || _disableColCountCheck)) { - var col = new ColImpl(this, colIndex, string.Empty, SepStringBuilderPool.Take()); + var col = new ColImpl(this, colIndex, string.Empty); _cols.Add(col); } return cols[colIndex]; @@ -122,7 +122,7 @@ internal ColImpl GetOrAddCol(string colName) internal ColImpl AddCol(string colName) { var colIndex = _colNameToCol.Count; - var col = new ColImpl(this, colIndex, colName, SepStringBuilderPool.Take()); + var col = new ColImpl(this, colIndex, colName); _colNameToCol.Add(colName, col); _cols.Add(col); _colNameCache.Add((colName, colIndex)); diff --git a/src/Sep/SepWriter.cs b/src/Sep/SepWriter.cs index f1324383..f15b5c73 100644 --- a/src/Sep/SepWriter.cs +++ b/src/Sep/SepWriter.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using System.Runtime.CompilerServices; -using System.Text; namespace nietras.SeparatedValues; @@ -99,17 +98,14 @@ internal void EndRow(Row row) { _writer.Write(_sep.Separator); } - var sb = col.Text; + var span = col.GetSpan(); if (_escape) { - WriteEscaped(sb); + WriteEscaped(span); } else { - foreach (var chunk in sb.GetChunks()) - { - _writer.Write(chunk.Span); - } + _writer.Write(span); } notFirst = true; } @@ -169,35 +165,6 @@ internal void WriteHeader() _headerWrittenOrSkipped = true; } - void WriteEscaped(StringBuilder sb) - { - var separator = _sep.Separator; - uint containsSpecialChar = 0; - - foreach (var chunk in sb.GetChunks()) - { - containsSpecialChar |= ContainsSpecialCharacters(chunk.Span, separator); - if (containsSpecialChar != 0) { break; } - } - - if (containsSpecialChar != 0) - { - _writer.Write(SepDefaults.Quote); - foreach (var chunk in sb.GetChunks()) - { - WriteQuotesEscaped(chunk.Span); - } - _writer.Write(SepDefaults.Quote); - } - else - { - foreach (var chunk in sb.GetChunks()) - { - _writer.Write(chunk.Span); - } - } - } - void WriteEscaped(ReadOnlySpan span) { var containsSpecialChar = ContainsSpecialCharacters(span, _sep.Separator); @@ -291,7 +258,7 @@ void DisposeManaged() _arrayPool.Dispose(); foreach (var col in _colNameToCol.Values) { - SepStringBuilderPool.Return(col.Text); + col.Dispose(); } _colNameToCol.Clear(); }