Skip to content

Commit ed3d344

Browse files
Copilotpetesramek
andauthored
perf: stack-allocate scratch spans and grow output buffer on demand in PolylineEncoder
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/9ad635cf-efe4-4650-86cc-0083af3375f7 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent e8262f6 commit ed3d344

1 file changed

Lines changed: 43 additions & 109 deletions

File tree

src/PolylineAlgorithm/PolylineEncoder.cs

Lines changed: 43 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ namespace PolylineAlgorithm;
77

88
using Microsoft.Extensions.Logging;
99
using PolylineAlgorithm.Abstraction;
10-
using PolylineAlgorithm.Internal;
1110
using PolylineAlgorithm.Internal.Diagnostics;
1211
using System;
1312
using System.Buffers;
1413
using System.Collections.Generic;
15-
using System.Diagnostics;
16-
using System.Runtime.CompilerServices;
1714
using System.Threading;
1815

1916
/// <summary>
@@ -67,74 +64,18 @@ public PolylineEncoder(PolylineOptions<TValue, TPolyline> options) {
6764
/// <exception cref="ArgumentException">
6865
/// Thrown when <paramref name="coordinates"/> is empty.
6966
/// </exception>
70-
/// <exception cref="InvalidOperationException">
71-
/// Thrown when the internal encoding buffer cannot accommodate the encoded value.
72-
/// </exception>
7367
/// <exception cref="OperationCanceledException">
7468
/// Thrown when <paramref name="cancellationToken"/> is canceled.
7569
/// </exception>
70+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")]
7671
public TPolyline Encode(IEnumerable<TValue> coordinates, CancellationToken cancellationToken = default) {
77-
const string OperationName = nameof(Encode);
78-
79-
_logger.LogOperationStartedDebug(OperationName);
72+
_logger.LogOperationStartedDebug(nameof(Encode));
8073

8174
if (coordinates is null) {
8275
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
8376
}
8477

85-
IReadOnlyList<TValue> items = coordinates as IReadOnlyList<TValue> ?? [.. coordinates];
86-
87-
if (items.Count < 1) {
88-
_logger.LogOperationFailedDebug(OperationName);
89-
_logger.LogEmptyArgumentWarning(nameof(coordinates));
90-
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
91-
}
92-
93-
int width = _formatter.Width;
94-
int length = GetMaxBufferLength(items.Count, width);
95-
96-
char[]? temp = length <= _options.StackAllocLimit
97-
? null
98-
: ArrayPool<char>.Shared.Rent(length);
99-
100-
Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);
101-
102-
int position = 0;
103-
long[] previous = new long[width];
104-
long[] values = new long[width];
105-
106-
SeedPrevious(previous, null);
107-
108-
try {
109-
for (int i = 0; i < items.Count; i++) {
110-
cancellationToken.ThrowIfCancellationRequested();
111-
112-
_formatter.GetValues(items[i], values.AsSpan());
113-
114-
for (int j = 0; j < width; j++) {
115-
long current = values[j];
116-
long delta = current - previous[j];
117-
previous[j] = current;
118-
119-
if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
120-
_logger.LogOperationFailedDebug(OperationName);
121-
_logger.LogCannotWriteValueToBufferWarning(position, i);
122-
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
123-
}
124-
}
125-
}
126-
127-
// Convert to string inside the try block so the buffer is still valid.
128-
string encodedResult = buffer[..position].ToString();
129-
130-
_logger.LogOperationFinishedDebug(OperationName);
131-
132-
return _formatter.Write(encodedResult.AsMemory());
133-
} finally {
134-
if (temp is not null) {
135-
ArrayPool<char>.Shared.Return(temp);
136-
}
137-
}
78+
return EncodeCore(coordinates, null, cancellationToken);
13879
}
13980

14081
/// <summary>
@@ -160,96 +101,89 @@ public TPolyline Encode(IEnumerable<TValue> coordinates, CancellationToken cance
160101
/// <exception cref="ArgumentException">
161102
/// Thrown when <paramref name="coordinates"/> is empty.
162103
/// </exception>
163-
/// <exception cref="InvalidOperationException">
164-
/// Thrown when the internal encoding buffer cannot accommodate the encoded value.
165-
/// </exception>
166104
/// <exception cref="OperationCanceledException">
167105
/// Thrown when <paramref name="cancellationToken"/> is canceled.
168106
/// </exception>
107+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")]
169108
public TPolyline Encode(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
170-
const string OperationName = nameof(Encode);
171-
172-
_logger.LogOperationStartedDebug(OperationName);
109+
_logger.LogOperationStartedDebug(nameof(Encode));
173110

174111
if (coordinates is null) {
175112
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
176113
}
177114

178-
IReadOnlyList<TValue> items = coordinates as IReadOnlyList<TValue> ?? [.. coordinates];
115+
return EncodeCore(coordinates, options, cancellationToken);
116+
}
179117

180-
if (items.Count < 1) {
181-
_logger.LogOperationFailedDebug(OperationName);
182-
_logger.LogEmptyArgumentWarning(nameof(coordinates));
183-
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
184-
}
118+
private TPolyline EncodeCore(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
119+
const string OperationName = nameof(Encode);
120+
const int MaxStackWidth = 8;
185121

186122
int width = _formatter.Width;
187-
int length = GetMaxBufferLength(items.Count, width);
188123

189-
char[]? temp = length <= _options.StackAllocLimit
190-
? null
191-
: ArrayPool<char>.Shared.Rent(length);
124+
Span<long> previous = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width];
125+
Span<long> values = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width];
126+
previous = previous[..width];
127+
values = values[..width];
192128

193-
Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);
129+
SeedPrevious(previous, options);
194130

195-
int position = 0;
196-
long[] previous = new long[width];
197-
long[] values = new long[width];
131+
int stackLimit = _options.StackAllocLimit;
132+
Span<char> buffer = stackalloc char[stackLimit];
133+
char[]? rented = null;
198134

199-
SeedPrevious(previous, options);
135+
int position = 0;
136+
bool hasItems = false;
200137

201138
try {
202-
for (int i = 0; i < items.Count; i++) {
139+
foreach (TValue item in coordinates) {
203140
cancellationToken.ThrowIfCancellationRequested();
204-
205-
_formatter.GetValues(items[i], values.AsSpan());
141+
hasItems = true;
142+
_formatter.GetValues(item, values);
206143

207144
for (int j = 0; j < width; j++) {
208145
long current = values[j];
209146
long delta = current - previous[j];
210147
previous[j] = current;
211148

212-
if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
213-
_logger.LogOperationFailedDebug(OperationName);
214-
_logger.LogCannotWriteValueToBufferWarning(position, i);
215-
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
149+
while (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
150+
int newSize = rented is null ? stackLimit * 2 : rented.Length * 2;
151+
char[] newRented = ArrayPool<char>.Shared.Rent(newSize);
152+
buffer[..position].CopyTo(newRented);
153+
if (rented is not null) {
154+
ArrayPool<char>.Shared.Return(rented);
155+
}
156+
rented = newRented;
157+
buffer = rented.AsSpan();
216158
}
217159
}
218160
}
219161

162+
if (!hasItems) {
163+
_logger.LogOperationFailedDebug(OperationName);
164+
_logger.LogEmptyArgumentWarning(nameof(coordinates));
165+
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
166+
}
167+
220168
string encodedResult = buffer[..position].ToString();
221169

222170
_logger.LogOperationFinishedDebug(OperationName);
223171

224172
return _formatter.Write(encodedResult.AsMemory());
225173
} finally {
226-
if (temp is not null) {
227-
ArrayPool<char>.Shared.Return(temp);
174+
if (rented is not null) {
175+
ArrayPool<char>.Shared.Return(rented);
228176
}
229177
}
230178
}
231179

232-
private void SeedPrevious(long[] previous, PolylineEncodingOptions<TValue>? options) {
233-
int width = _formatter.Width;
234-
180+
private void SeedPrevious(Span<long> previous, PolylineEncodingOptions<TValue>? options) {
235181
if (options is { HasPrevious: true }) {
236-
_formatter.GetValues(options.Previous, previous.AsSpan());
182+
_formatter.GetValues(options.Previous, previous);
237183
} else {
238-
for (int j = 0; j < width; j++) {
184+
for (int j = 0; j < previous.Length; j++) {
239185
previous[j] = _formatter.GetBaseline(j);
240186
}
241187
}
242188
}
243-
244-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
245-
private static int GetMaxBufferLength(int count, int valuesPerItem) {
246-
Debug.Assert(count > 0, "Count must be greater than zero.");
247-
Debug.Assert(valuesPerItem > 0, "Values per item must be greater than zero.");
248-
249-
int requestedBufferLength = count * valuesPerItem * Defaults.Polyline.Block.Length.Max;
250-
251-
Debug.Assert(requestedBufferLength > 0, "Requested buffer length must be greater than zero.");
252-
253-
return requestedBufferLength;
254-
}
255189
}

0 commit comments

Comments
 (0)