|
| 1 | +# Buffers and pooled strings |
| 2 | + |
| 3 | +`SquidStd.Core.Buffers` and `SquidStd.Core.Extensions.Strings` are a small, dependency-free toolkit for allocation-sensitive string and byte work: a single-threaded array pool, a stack-friendly string builder, and a family of ordinal/case-insensitive string helpers that skip culture-aware comparisons entirely. |
| 4 | + |
| 5 | +## When to reach for these types |
| 6 | + |
| 7 | +Reach for this toolkit on hot paths that would otherwise allocate a `char[]` or `byte[]` on every call: parsers, network frame encoders/decoders, per-tick game loop work, or any single-threaded pipeline that runs often enough for GC pressure to matter. For one-off, cold-path string work, `string`, `StringBuilder`, and `ArrayPool<T>.Shared` are simpler and perfectly fine. |
| 8 | + |
| 9 | +## STArrayPool |
| 10 | + |
| 11 | +`STArrayPool<T>` is a single-threaded adaptation of the .NET runtime's shared `ArrayPool<T>` (`TlsOverPerCoreLockedStacksArrayPool`). It keeps the same bucketing scheme - powers of two, from 16 elements up to roughly 1 GiB - and the same rent/return API, but drops every lock: there is no per-core or thread-local synchronization at all. |
| 12 | + |
| 13 | +> [!WARNING] |
| 14 | +> `STArrayPool<T>.Shared` is NOT thread-safe. It performs no locking, so renting or returning from more than one thread at a time is a race - it can hand out the same array twice or corrupt its internal buckets. Only use it from code that already guarantees exclusive, single-threaded access. |
| 15 | +
|
| 16 | +Use the decision table to pick the right pool: |
| 17 | + |
| 18 | +| Scenario | Use | |
| 19 | +|---|---| |
| 20 | +| A single-threaded hot path that owns its buffer exclusively (a parser, formatter, or one tick of a game loop) | `STArrayPool<T>.Shared` | |
| 21 | +| Any code that might run concurrently, or where you can't prove exclusive ownership | `ArrayPool<T>.Shared` | |
| 22 | + |
| 23 | +`STArrayPool<T>` trims itself automatically: the first time it caches a returned array it registers a Gen2 GC callback, and every Gen2 collection runs `Trim()`. `Trim()` reads `GC.GetGCMemoryInfo()` and classifies the current memory load into `Low`, `Medium`, or `High` pressure (at or above 70% and 90% of the high memory-load threshold). Under `High` pressure the pool clears its single-slot fast-path cache immediately and ages arrays out of the per-size overflow stacks after only 10 seconds. Under `Medium` and `Low` pressure it is gentler: the fast-path cache ages out after 10 or 30 seconds of sitting idle, and the overflow stacks age out after 60 seconds. This keeps a short burst of activity from being trimmed away between GCs while still giving memory back once the process is actually under pressure. |
| 24 | + |
| 25 | +`Return` does not clear the array's contents unless you pass `clearArray: true`. For arrays of a reference type, that means previously stored object references stay reachable through the pool until the array is rented out again (and overwritten) or discarded by a trim - pass `clearArray: true` when that matters. |
| 26 | + |
| 27 | +```csharp |
| 28 | +using SquidStd.Core.Buffers; |
| 29 | + |
| 30 | +var array = STArrayPool<byte>.Shared.Rent(1024); |
| 31 | +try |
| 32 | +{ |
| 33 | + // use array... |
| 34 | +} |
| 35 | +finally |
| 36 | +{ |
| 37 | + STArrayPool<byte>.Shared.Return(array); |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +## ValueStringBuilder |
| 42 | + |
| 43 | +`ValueStringBuilder` is a `ref struct` string builder backed by a single growable `Span<char>` instead of `StringBuilder`'s linked chunks. Being a ref struct, it must stay on the stack: it cannot be boxed, stored in a field of a non-ref struct, or captured by a lambda or async method. |
| 44 | + |
| 45 | +```csharp |
| 46 | +using SquidStd.Core.Buffers; |
| 47 | + |
| 48 | +using var builder = new ValueStringBuilder(stackalloc char[64]); |
| 49 | + |
| 50 | +builder.Append("hello "); |
| 51 | +builder.Append($"world {42}"); |
| 52 | + |
| 53 | +var text = builder.ToString(); // copies the written span into a new string |
| 54 | +``` |
| 55 | + |
| 56 | +`ToString()` only reads the builder's contents - it does not release the buffer. Wrap the builder in a `using` declaration (as above) so `Dispose()` returns any rented buffer to the pool once you're done; if the builder never grew past its initial `stackalloc` span, `Dispose()` is a harmless no-op. |
| 57 | + |
| 58 | +By default (`mt: false`), the builder rents from `STArrayPool<char>.Shared`, so the same single-threaded rule from the previous section applies: create, append to, and dispose the builder from one thread. Pass `mt: true` when the builder - or a buffer it grew into - might cross threads; this switches renting to `ArrayPool<char>.Shared` instead: |
| 59 | + |
| 60 | +```csharp |
| 61 | +var builder = new ValueStringBuilder(64, mt: true); |
| 62 | +builder.Append("thread safe pool path"); |
| 63 | +var text = builder.ToString(); |
| 64 | +builder.Dispose(); |
| 65 | +``` |
| 66 | + |
| 67 | +## Pooled helpers |
| 68 | + |
| 69 | +`ToPooledArray` copies a string into a buffer rented from `STArrayPool<char>.Shared`. The caller owns the returned array and must return it; the buffer may be longer than the source string, so only the first `str.Length` characters are valid. |
| 70 | + |
| 71 | +```csharp |
| 72 | +using SquidStd.Core.Buffers; |
| 73 | +using SquidStd.Core.Extensions.Strings; |
| 74 | + |
| 75 | +var array = "hello world".ToPooledArray(); |
| 76 | +try |
| 77 | +{ |
| 78 | + // only array[..11] holds the copied characters |
| 79 | +} |
| 80 | +finally |
| 81 | +{ |
| 82 | + STArrayPool<char>.Shared.Return(array); |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +`PooledArraySpanFormattable` wraps a `STArrayPool<char>`-rented buffer as an `ISpanFormattable`, so it can flow through interpolated string handlers and span-formatting APIs without allocating an intermediate string. `TryFormat` is single-use: a successful call copies the characters into the destination and returns the buffer to the pool as part of that same call, so the wrapper must not be used again afterward. `ToString()` is idempotent instead - the first call caches the string and releases the buffer, and later calls return the same cached instance. |
| 87 | + |
| 88 | +## String helpers |
| 89 | + |
| 90 | +`OrdinalStringHelpers` and `InsensitiveStringHelpers` mirror each other: one comparison family, ordinal (case-sensitive) on one side and ordinal-ignore-case on the other, both skipping culture lookups entirely. Every member works on `string`, and most also have a `ReadOnlySpan<char>` overload. |
| 91 | + |
| 92 | +| Family | Ordinal | Insensitive | |
| 93 | +|---|---|---| |
| 94 | +| Starts with | `StartsWithOrdinal` | `InsensitiveStartsWith` | |
| 95 | +| Ends with | `EndsWithOrdinal` | `InsensitiveEndsWith` | |
| 96 | +| Equals | `EqualsOrdinal` | `InsensitiveEquals` | |
| 97 | +| Contains | `ContainsOrdinal` | `InsensitiveContains` | |
| 98 | +| Compare | `CompareOrdinal` | `InsensitiveCompare` | |
| 99 | +| Index of | `IndexOfOrdinal` | `InsensitiveIndexOf` | |
| 100 | +| Remove | `RemoveOrdinal` | `InsensitiveRemove` | |
| 101 | +| Replace | `ReplaceOrdinal` | `InsensitiveReplace` | |
| 102 | + |
| 103 | +```csharp |
| 104 | +using SquidStd.Core.Extensions.Strings; |
| 105 | + |
| 106 | +"hello world".StartsWithOrdinal("hello"); // true, no culture lookup |
| 107 | +"Hello".InsensitiveEquals("hELLO"); // true |
| 108 | +``` |
| 109 | + |
| 110 | +`StringHelpers` rounds out the set with general-purpose utilities: |
| 111 | + |
| 112 | +- `Capitalize` - title-cases every space-separated word, leaving "the " untouched. |
| 113 | +- `Wrap` - word-wraps text into lines of at most N characters, hard-breaking words that don't fit on their own line. |
| 114 | +- `IndentMultiline` - prefixes every line of a multiline string with an indent. |
| 115 | +- `TrimMultiline` - trims every line of a multiline string independently. |
| 116 | +- `IndexOfTerminator` - finds a null terminator in a byte/char/uint buffer. |
| 117 | +- `ReplaceAny` - replaces every occurrence of a set of characters with their paired replacements, in place only. |
| 118 | + |
| 119 | +## Related |
| 120 | + |
| 121 | +- [SquidStd.Core](../core.md) - the package these types ship in, alongside the rest of the dependency-free helpers. |
| 122 | +- [Home](../../index.md) - back to the SquidStd documentation home. |
0 commit comments