Skip to content

Commit fc751ae

Browse files
Copilotpetesramek
andauthored
feat: remove IPolylineWriter/IPolylineReader; restore stackalloc in encoder via ref struct PolylineWriter
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/8085d58f-4c97-4a7a-a577-f078ff234b2a Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 9af79d9 commit fc751ae

16 files changed

Lines changed: 61 additions & 131 deletions

File tree

benchmarks/PolylineAlgorithm.Benchmarks/PolylineDecoderBenchmark.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace PolylineAlgorithm.Benchmarks;
88
using BenchmarkDotNet.Attributes;
99
using BenchmarkDotNet.Engines;
1010
using PolylineAlgorithm.Abstraction;
11+
using PolylineAlgorithm.Internal;
1112
using PolylineAlgorithm.Utility;
1213

1314
/// <summary>
@@ -93,7 +94,7 @@ public void PolylineDecoder_Decode_Memory() {
9394
}
9495

9596
private sealed class StringPolylineDecoder : AbstractPolylineDecoder<string, (double Latitude, double Longitude)> {
96-
protected override (double Latitude, double Longitude) Read(IPolylineReader reader) =>
97+
protected override (double Latitude, double Longitude) Read(PolylineReader reader) =>
9798
(reader.Read(), reader.Read());
9899

99100
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
@@ -102,7 +103,7 @@ protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
102103
}
103104

104105
private sealed class CharArrayPolylineDecoder : AbstractPolylineDecoder<char[], (double Latitude, double Longitude)> {
105-
protected override (double Latitude, double Longitude) Read(IPolylineReader reader) =>
106+
protected override (double Latitude, double Longitude) Read(PolylineReader reader) =>
106107
(reader.Read(), reader.Read());
107108

108109
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in char[] polyline) {
@@ -111,7 +112,7 @@ protected override ReadOnlyMemory<char> GetReadOnlyMemory(in char[] polyline) {
111112
}
112113

113114
private sealed class MemoryCharPolylineDecoder : AbstractPolylineDecoder<ReadOnlyMemory<char>, (double Latitude, double Longitude)> {
114-
protected override (double Latitude, double Longitude) Read(IPolylineReader reader) =>
115+
protected override (double Latitude, double Longitude) Read(PolylineReader reader) =>
115116
(reader.Read(), reader.Read());
116117

117118
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in ReadOnlyMemory<char> polyline) {

benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace PolylineAlgorithm.Benchmarks;
99
using BenchmarkDotNet.Engines;
1010
using PolylineAlgorithm.Abstraction;
1111
using PolylineAlgorithm.Extensions;
12+
using PolylineAlgorithm.Internal;
1213
using PolylineAlgorithm.Utility;
1314
using System.Collections.Generic;
1415

@@ -85,8 +86,8 @@ public void PolylineEncoder_Encode_List() {
8586
}
8687

8788
private sealed class StringPolylineEncoder : AbstractPolylineEncoder<(double Latitude, double Longitude), string> {
88-
protected override string CreatePolyline(ReadOnlyMemory<char> polyline) => polyline.ToString();
89-
protected override void Write((double Latitude, double Longitude) item, IPolylineWriter writer) {
89+
protected override string CreatePolyline(ReadOnlySpan<char> polyline) => polyline.ToString();
90+
protected override void Write((double Latitude, double Longitude) item, ref PolylineWriter writer) {
9091
writer.Write(item.Latitude);
9192
writer.Write(item.Longitude);
9293
}

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineDecoder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
77

88
using global::NetTopologySuite.Geometries;
99
using PolylineAlgorithm.Abstraction;
10+
using PolylineAlgorithm.Internal;
1011
using System;
1112

1213
/// <summary>
@@ -27,7 +28,7 @@ protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
2728
/// </summary>
2829
/// <param name="reader">The reader provided by the engine. Field 0 = latitude, field 1 = longitude.</param>
2930
/// <returns>Point instance.</returns>
30-
protected override Point Read(IPolylineReader reader) {
31+
protected override Point Read(PolylineReader reader) {
3132
double latitude = reader.Read();
3233
double longitude = reader.Read();
3334

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineEncoder.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
77

88
using global::NetTopologySuite.Geometries;
99
using PolylineAlgorithm.Abstraction;
10+
using PolylineAlgorithm.Internal;
1011
using System;
1112

1213
/// <summary>
1314
/// Polyline encoder using NetTopologySuite's Point type.
1415
/// </summary>
1516
internal sealed class NetTopologyPolylineEncoder : AbstractPolylineEncoder<Point, string> {
1617
/// <summary>
17-
/// Creates encoded polyline string from memory.
18+
/// Creates encoded polyline string from span.
1819
/// </summary>
19-
/// <param name="polyline">Polyline memory.</param>
20+
/// <param name="polyline">Polyline span.</param>
2021
/// <returns>Encoded polyline string.</returns>
21-
protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
22+
protected override string CreatePolyline(ReadOnlySpan<char> polyline) {
2223
if (polyline.IsEmpty) {
2324
return string.Empty;
2425
}
@@ -31,7 +32,7 @@ protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
3132
/// </summary>
3233
/// <param name="item">The point to write. Field 0 = latitude (Y), field 1 = longitude (X).</param>
3334
/// <param name="writer">The writer provided by the engine.</param>
34-
protected override void Write(Point item, IPolylineWriter writer) {
35+
protected override void Write(Point item, ref PolylineWriter writer) {
3536
ArgumentNullException.ThrowIfNull(item);
3637

3738
// NetTopologySuite Point: Y = latitude, X = longitude

src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,18 @@ protected virtual void ValidateFormat(ReadOnlyMemory<char> sequence, ILogger? lo
181181
/// Reads field values from the polyline decoding pipeline and constructs one <typeparamref name="TCoordinate"/> item.
182182
/// </summary>
183183
/// <param name="reader">
184-
/// The <see cref="IPolylineReader"/> cursor provided by the engine. Call <see cref="IPolylineReader.Read"/>
184+
/// The <see cref="PolylineReader"/> cursor provided by the engine. Call <see cref="PolylineReader.Read"/>
185185
/// once for each expected field value, in the same order used by the corresponding encoder's
186186
/// <see cref="Write"/> override.
187187
/// </param>
188188
/// <returns>
189189
/// A <typeparamref name="TCoordinate"/> instance constructed from the decoded field values.
190190
/// </returns>
191191
/// <remarks>
192-
/// Implementations must always call <see cref="IPolylineReader.Read"/> the same number of times,
192+
/// Implementations must always call <see cref="PolylineReader.Read"/> the same number of times,
193193
/// in the same field order, for every item. The number of reads must match the number of writes
194194
/// performed by the corresponding encoder's <see cref="Write"/> override.
195195
/// </remarks>
196196
[MethodImpl(MethodImplOptions.AggressiveInlining)]
197-
protected abstract TCoordinate Read(IPolylineReader reader);
197+
protected abstract TCoordinate Read(PolylineReader reader);
198198
}

src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,12 @@ public TPolyline Encode(ReadOnlySpan<TCoordinate> coordinates, CancellationToken
9090
// Worst-case maximum: every value uses the maximum number of encoded characters.
9191
int maxCapacity = coordinates.Length * 2 * Defaults.Polyline.Block.Length.Max;
9292

93-
// Use ArrayPool for large buffers to avoid large heap allocations; for small buffers a fresh
94-
// allocation is cheaper than pool overhead.
93+
// Use stackalloc for small buffers (zero heap allocation); fall back to ArrayPool for large ones.
9594
const int StackAllocThreshold = 512;
96-
char[]? rentedBuffer = maxCapacity > StackAllocThreshold
97-
? ArrayPool<char>.Shared.Rent(maxCapacity)
98-
: null;
99-
char[] buffer = rentedBuffer ?? new char[maxCapacity];
95+
char[]? rentedBuffer = null;
96+
Span<char> buffer = maxCapacity > StackAllocThreshold
97+
? (rentedBuffer = ArrayPool<char>.Shared.Rent(maxCapacity)).AsSpan(0, maxCapacity)
98+
: stackalloc char[maxCapacity];
10099

101100
PolylineWriter writer = new(buffer, Options.Precision);
102101

@@ -107,10 +106,10 @@ public TPolyline Encode(ReadOnlySpan<TCoordinate> coordinates, CancellationToken
107106
cancellationToken.ThrowIfCancellationRequested();
108107

109108
writer.BeginItem();
110-
Write(coordinates[i], writer);
109+
Write(coordinates[i], ref writer);
111110
}
112111

113-
result = CreatePolyline(writer.WrittenMemory);
112+
result = CreatePolyline(writer.WrittenSpan);
114113
} finally {
115114
if (rentedBuffer is not null) {
116115
ArrayPool<char>.Shared.Return(rentedBuffer);
@@ -136,29 +135,29 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan<TCoordinate> coordinates,
136135
}
137136

138137
/// <summary>
139-
/// Creates a polyline instance from the provided read-only sequence of characters.
138+
/// Creates a polyline instance from the provided read-only span of characters.
140139
/// </summary>
141-
/// <param name="polyline">A <see cref="ReadOnlyMemory{T}"/> containing the encoded polyline characters.</param>
140+
/// <param name="polyline">A <see cref="ReadOnlySpan{T}"/> containing the encoded polyline characters.</param>
142141
/// <returns>
143142
/// An instance of <typeparamref name="TPolyline"/> representing the encoded polyline.
144143
/// </returns>
145144
[MethodImpl(MethodImplOptions.AggressiveInlining)]
146-
protected abstract TPolyline CreatePolyline(ReadOnlyMemory<char> polyline);
145+
protected abstract TPolyline CreatePolyline(ReadOnlySpan<char> polyline);
147146

148147
/// <summary>
149148
/// Writes the field values of the specified item into the polyline encoding pipeline.
150149
/// </summary>
151150
/// <param name="item">The item whose field values are to be encoded.</param>
152151
/// <param name="writer">
153-
/// The <see cref="IPolylineWriter"/> cursor provided by the engine. Call <see cref="IPolylineWriter.Write"/>
152+
/// The <see cref="PolylineWriter"/> cursor provided by the engine. Call <see cref="PolylineWriter.Write"/>
154153
/// once for each field value, in a fixed, consistent order. The engine handles delta computation,
155154
/// zigzag encoding, and output buffering.
156155
/// </param>
157156
/// <remarks>
158-
/// Implementations must always call <see cref="IPolylineWriter.Write"/> the same number of times,
157+
/// Implementations must always call <see cref="PolylineWriter.Write"/> the same number of times,
159158
/// in the same field order, for every item. The corresponding <see cref="Read"/> override must
160-
/// call <see cref="IPolylineReader.Read"/> the same number of times in the same order.
159+
/// call <see cref="PolylineReader.Read"/> the same number of times in the same order.
161160
/// </remarks>
162161
[MethodImpl(MethodImplOptions.AggressiveInlining)]
163-
protected abstract void Write(TCoordinate item, IPolylineWriter writer);
162+
protected abstract void Write(TCoordinate item, ref PolylineWriter writer);
164163
}

src/PolylineAlgorithm/Abstraction/IPolylineReader.cs

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/PolylineAlgorithm/Abstraction/IPolylineWriter.cs

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/PolylineAlgorithm/Internal/PolylineReader.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
namespace PolylineAlgorithm.Internal;
77

8-
using PolylineAlgorithm.Abstraction;
98
using PolylineAlgorithm.Internal.Diagnostics;
109
using System.Runtime.CompilerServices;
1110

@@ -18,7 +17,7 @@ namespace PolylineAlgorithm.Internal;
1817
/// The engine calls <see cref="BeginItem"/> before invoking the formatter for each item so that the
1918
/// slot index resets correctly while delta state is preserved across item boundaries.
2019
/// </remarks>
21-
internal sealed class PolylineReader : IPolylineReader {
20+
public sealed class PolylineReader {
2221
private readonly ReadOnlyMemory<char> _sequence;
2322
private int _position;
2423
private int[] _accumulated;

src/PolylineAlgorithm/Internal/PolylineWriter.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
namespace PolylineAlgorithm.Internal;
77

8-
using PolylineAlgorithm.Abstraction;
98
using PolylineAlgorithm.Internal.Diagnostics;
109
using System;
1110
using System.Runtime.CompilerServices;
@@ -15,18 +14,19 @@ namespace PolylineAlgorithm.Internal;
1514
/// </summary>
1615
/// <remarks>
1716
/// Each instance wraps a caller-provided <see cref="char"/> buffer sized to the worst-case maximum
18-
/// capacity so that the buffer never needs to grow. The engine calls <see cref="BeginItem"/> before
17+
/// capacity so that the buffer never needs to grow. The buffer may be stack-allocated or rented from
18+
/// <see cref="System.Buffers.ArrayPool{T}"/>; the engine calls <see cref="BeginItem"/> before
1919
/// invoking the formatter for each item so that the slot index resets correctly while delta state is
2020
/// preserved across item boundaries.
2121
/// </remarks>
22-
internal sealed class PolylineWriter : IPolylineWriter {
23-
private readonly char[] _buffer;
22+
public ref struct PolylineWriter {
23+
private Span<char> _buffer;
2424
private int _position;
2525
private readonly uint _precision;
2626
private int[] _previous;
2727
private int _slotIndex;
2828

29-
internal PolylineWriter(char[] buffer, uint precision) {
29+
internal PolylineWriter(Span<char> buffer, uint precision) {
3030
_buffer = buffer;
3131
_precision = precision;
3232
_previous = [];
@@ -48,7 +48,7 @@ public void Write(double value) {
4848
_previous[_slotIndex] = normalized;
4949
_slotIndex++;
5050

51-
if (!PolylineEncoding.TryWriteValue(delta, _buffer.AsSpan(), ref _position)) {
51+
if (!PolylineEncoding.TryWriteValue(delta, _buffer, ref _position)) {
5252
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
5353
}
5454
}
@@ -67,7 +67,6 @@ public void Write(double value) {
6767

6868
/// <summary>
6969
/// Returns the encoded polyline characters written so far.
70-
/// The caller must not use this memory after the buffer is returned to <see cref="System.Buffers.ArrayPool{T}"/>.
7170
/// </summary>
72-
internal ReadOnlyMemory<char> WrittenMemory => _buffer.AsMemory(0, _position);
71+
internal ReadOnlySpan<char> WrittenSpan => _buffer[.._position];
7372
}

0 commit comments

Comments
 (0)