A colleague asked several AIs what we might do to improve performance. I've dumped the results here. We should work through these to see which suggestions look worthwhile, and create separate work items if appropriate:
Gemini 3 Pro
Performance Improvements for Ais.Net
This document outlines suggested performance improvements for the Ais.Net library, targeting .NET 10 and C# 14. The goal is to achieve zero-allocation, high-performance code suitable for modern CPU architectures.
1. Modern .NET & C# Features
1.1. UTF-8 String Literals
Current: Encoding.ASCII.GetBytes("VDM") creates a byte array at runtime (or static init).
Recommendation: Use C# 11 UTF-8 string literals ("VDM"u8). This compiles directly to a ReadOnlySpan<byte> pointing to the data section of the assembly, avoiding array allocation and initialization overhead.
1.2. SequenceReader<T> for Parsing
Current: NmeaLineParser and NmeaTagBlockParser manually slice spans and search for delimiters.
Recommendation: Use System.Buffers.SequenceReader<byte>. It provides optimized methods for reading primitives, advancing, and searching, often using SIMD under the hood. It also handles ReadOnlySequence<byte> natively, which is crucial for fragmented data (see 2.1).
1.3. SkipLocalsInit
Current: Default zero-initialization of locals.
Recommendation: Apply [SkipLocalsInit] to performance-critical methods (like NmeaAisBitVectorParser.GetUnsignedInteger) to avoid the cost of zeroing stack memory, especially when using stackalloc.
1.4. SIMD Vectorization
Current: Scalar processing of bits and bytes.
Recommendation:
- Comma Finding: Use
Vector128<byte> or Vector256<byte> (via Vector128.Create((byte)',')) to find delimiters in NmeaLineParser in a single pass, rather than repeated IndexOf calls.
- 6-bit Decoding: Investigate using SIMD to decode multiple 6-bit ASCII characters in parallel. While bit-packing is tricky, AVX2/AVX-512 instructions (like
vpmultishiftqb or shuffle) can sometimes perform parallel bit extraction and packing.
2. Memory Management & Allocations
2.1. Zero-Copy Reassembly of Fragmented Messages
Current: NmeaLineToAisStreamAdapter allocates a new buffer for each fragment, copies data into it, then allocates a larger buffer to combine them, and copies again.
Recommendation:
- Store fragments as a list of
ReadOnlyMemory<byte> (or ReadOnlySequence<byte> segments).
- Construct a
ReadOnlySequence<byte> representing the logical contiguous payload.
- Update
NmeaAisBitVectorParser and NmeaPayloadParser to accept ReadOnlySequence<byte> (or use SequenceReader).
- This eliminates the large allocation and copy for reassembly.
2.2. Eliminate Dictionary in Hot Path
Current: NmeaLineToAisStreamAdapter uses Dictionary<int, FragmentedMessage> to track fragments.
Recommendation:
- If group IDs are small/dense, use an array.
- If sparse, consider a
FrozenDictionary (if static) or a specialized high-performance map (like SwissTable implementation or open addressing) to reduce overhead.
- Since this is a read-write cache, a pooled object approach for the
FragmentedMessage containers would reduce GC pressure.
2.3. Fix Repeated Parsing in NmeaLineParser.TagBlock
Current: Accessing parsedLine.TagBlock creates a new NmeaTagBlockParser which immediately parses the entire tag block. NmeaLineToAisStreamAdapter accesses this property multiple times per line, causing redundant parsing.
Recommendation:
- Change the pattern so the caller parses once and stores the result.
- Or, make
NmeaTagBlockParser lazy and lightweight (only parse when specific fields are requested), though this is harder with ref struct.
- Best approach: Parse tag block once into a struct of results if needed, or pass the
NmeaTagBlockParser instance around.
2.4. Lookup Table for 6-bit Decoding
Current: NmeaPayloadParser.AisAsciiTo6Bits uses nested ternary operators and branches.
Recommendation: Use a static ReadOnlySpan<byte> lookup table (size 128). This makes decoding branchless and extremely fast (O(1) memory access).
3. API & Architecture
3.1. Exception Handling
Current: NmeaLineParser and NmeaPayloadParser throw exceptions (ArgumentException, NotSupportedException) for invalid data.
Recommendation:
- In high-throughput streams, invalid data (noise) is common. Exceptions are extremely expensive (stack trace generation).
- Use a
TryCreate or TryParse pattern returning a bool or a status enum (OperationStatus).
- Only throw for truly exceptional, unrecoverable errors (e.g., internal state corruption), not for bad input data.
3.2. System.IO.Pipelines Optimization
Current: NmeaStreamParser manually creates a Pipe and a background task to copy Stream to Pipe.
Recommendation:
- Use
PipeReader.Create(Stream) which is optimized and handles buffering/copying efficiently.
- Avoid the manual
ProcessFileAsync loop.
- Tune buffer sizes dynamically or based on empirical data rather than hardcoded 1MB/1KB.
3.3. NmeaLineToAisStreamAdapter Redundant Parsing
Current: When reassembling, NmeaLineToAisStreamAdapter creates a new NmeaLineParser for each fragment to extract the payload.
Recommendation:
- The payload offset and length should be calculated once when the fragment is first received and stored in
FragmentedMessage.
- When reassembling, use these stored offsets to slice the buffer directly without re-parsing the NMEA structure.
4. Micro-Optimizations
GetSingleDigitField: Replace manual char math with u8 subtraction or Utf8Parser.
Math.Min: In NmeaAisBitVectorParser, Math.Min might add a branch. Bitwise operations can sometimes replace it.
stackalloc: Ensure stackalloc is used for small temporary buffers (like the fragment removal list) to avoid GC pressure.
MethodImplOptions.AggressiveInlining: Apply to small hot methods like AisAsciiTo6Bits (if not using lookup table) or bit extraction helpers.
5. Benchmarking Strategy
Before applying these changes:
- Run existing benchmarks in
Solutions/Ais.Net.Benchmarks.
- Create new benchmarks specifically for:
NmeaLineParser constructor (parsing speed).
NmeaAisBitVectorParser.GetUnsignedInteger (decoding speed).
NmeaLineToAisStreamAdapter (fragment reassembly overhead).
- Profile using
dotnet-trace or dotMemory to confirm allocation hotspots.
Opus 4.5
Solutions Performance Improvement Review (December 2025)
This document captures a targeted performance review of the code under Solutions, with concrete modernization actions to keep the pipeline “zero allocation” friendly on .NET 10/C# 14 era runtimes. The review prioritizes hot paths (stream ingestion, parsing, fragment reassembly) and supporting assets (benchmarks, build configuration).
Quick wins (highest ROI first)
| # |
Area |
File / Location |
Issue |
Recommendation |
Expected impact |
| 1 |
Targeting & packaging |
Ais.Net/Ais.Net.csproj lines 14‑24; Common.*.proj |
Library still targets only netstandard2.x, pulling in System.IO.Pipelines 4.7.5 and System.Memory shims. Modern JIT/SIMD, CollectionsMarshal, ValueStringBuilder, NativeAOT, etc. are unavailable. |
Multi-target net10.0;net8.0;netstandard2.1;netstandard2.0 and conditionally light up modern intrinsics (#if NET8_0_OR_GREATER). Drop legacy package references where the runtime already provides them. |
Unlocks new BCL intrinsics, reduces package graph, and enables shipping NativeAOT-optimized assets without forking the codebase. |
| 2 |
Stream ingestion & I/O |
Ais.Net/Ais/Net/NmeaStreamParser.cs lines 108‑255 |
Manual Pipe bridge reads with FileStream bufferSize 1, Environment.TickCount, and a fixed 1 kB splitLineBuffer. This causes excess syscalls, bounded line lengths, and extra parsing passes for multi-segment sequences. |
Use FileStreamOptions (async + SequentialScan) plus PipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: 4 MB, minimumReadSize: 128 kB)). Replace splitLineBuffer with pooled/stack spans sized to actual line length, and switch timing to Stopwatch.GetTimestamp()/ValueStopwatch. |
Higher throughput from fewer kernel transitions, removal of per-line heap allocations, safe handling of arbitrarily long NMEA sentences, and precise telemetry even on long-running services. |
| 3 |
Fragment reassembly |
Ais.Net/Ais/Net/NmeaLineToAisStreamAdapter.cs lines 81‑238 |
Every fragment is reparsed several times, entire NMEA lines are copied into pooled arrays, and dictionary lookups perform multiple allocations; stackalloc removal list can blow the stack under fan-in bursts. |
Cache NmeaTagBlockParser per line, store only payload slices + padding, and replace dictionary logic with CollectionsMarshal.GetValueRefOrAddDefault. Use pooled ValueListBuilder<int> (or ArrayPool<int>) for aged fragment tracking. |
Drops reparse cost, halves pooled memory pressure, and avoids GC pressure / stack overflows when thousands of fragments are inflight. |
| 4 |
Bit-vector decoding |
Ais.Net/Ais/Net/NmeaAisBitVectorParser.cs lines 26‑147; NmeaPayloadParser.cs lines 22‑45 |
Each field extraction reconverts ASCII → 6-bit via branchy logic and shifts at most 6 bits per loop, leading to ~5–7× more instructions than necessary. |
When constructing the parser, decode the entire payload once into a Span<uint> of 6-bit values (using stackalloc up to ~256 B, else ArrayPool). Use BinaryPrimitives.ReadUInt32BigEndian/BitOperations.RotateLeft to pull up to 32 bits at once and implement sign-extension via arithmetic shifts. |
Eliminates repeated ASCII decoding, enabling vectorization and measurable reductions (>20%) in CPU cycles per field-heavy message. |
| 5 |
Benchmark harness |
Ais.Net.Benchmarks/*.cs |
Benchmark setup rewrites a 1 M line file synchronously every run and does not emit GC/improvement counters, making regressions hard to spot. |
Pre-build datasets once per job (or use memory-mapped files) and wire BenchmarkDotNet EventPipeProfiler/HardwareCounters. Add scenarios that exercise multi-threaded ingestion and NativeAOT builds. |
Produces more stable perf baselines and surfaces instruction/IPC deltas when applying the other optimizations. |
Detailed findings
1. Target frameworks, analyzers, and packages
Solutions/Ais.Net/Ais.Net.csproj (lines 14‑24) still targets only netstandard2.0/2.1, forcing the inclusion of System.IO.Pipelines 4.7.5 and System.Memory for legacy TFMs. This blocks the use of modern APIs such as CollectionsMarshal, System.Buffers.SearchValues, Span<T>-friendly regexes, RandomAccess.ReadAsync, or NativeAOT-compatible trimming hints.
- Recommendation:
- Multi-target
net10.0;net8.0 alongside the existing netstandard TFMs, and use <RuntimeIdentifier>-specific PublishAot profiles for deployments where latency matters.
- Move shared analyzer/package configuration out of
Common.Net.proj once per-SDK analyzers (e.g., CA.Aot) are enabled so we can enable new warnings only where supported.
- Replace
System.IO.Pipelines NuGet dependency with the in-box version for modern TFMs, while keeping the package reference only for netstandard2.0.
Impact: The compiler can now emit intrinsics (AVX‑512 on Linux, ARM64 SVE), and we reduce package restore time and dependency graph complexity.
2. Streaming and I/O hot path (NmeaStreamParser.cs)
Observed issues:
- Lines 108‑205:
ParseStreamAsync polls PipeReader.ReadAsync without ReadAtLeastAsync, resulting in tiny reads on slow disks and extra syscalls. Also, the manual ProcessBuffer repeatedly scans for \n using ReadOnlySequence.PositionOf, which walks the sequence twice.
- Lines 79‑81:
new FileStream(path, … bufferSize: 1, useAsync: true) disables user-mode buffering but increases kernel transitions. Modern .NET exposes FileStreamOptions that preserve zero-copy semantics without artificially shrinking the buffer.
- Line 116:
byte[] splitLineBuffer = new byte[1000]; is allocated per call and fails on >1 kB sentences, requiring reallocation (and silently truncating when line.Length > 1000).
- Lines 131‑200: timing uses
Environment.TickCount, which wraps every 49 days and lacks resolution; progress reports will become negative on busy daemons.
- Lines 217‑235: the custom
PipeWriter loop calls writer.GetMemory() with unspecified size, leading to 4 kB acquisitions on most platforms and inflated flush frequency.
Recommendations:
- Replace the manual pipe with
PipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: 4 * 1024 * 1024, minimumReadSize: 128 * 1024, leaveOpen: true)) and use SequenceReader<byte> to parse lines without copying, drastically reducing branch mispredictions.
- Use
FileStreamOptions (async + SequentialScan) or RandomAccess.ReadAsync to batch filesystem IO while still avoiding double-buffering.
- Replace the fixed
splitLineBuffer with Span<byte> lineBuffer = line.IsSingleSegment ? line.FirstSpan : ArrayPool<byte>.Shared.Rent((int)line.Length) plus try/finally return, or adopt ValueListBuilder<byte> to keep short lines on the stack.
- Switch timing to
ValueStopwatch/Stopwatch.GetTimestamp() and compute throughput via Stopwatch.Frequency, eliminating wrap-around bugs.
- Request larger chunks from the
PipeWriter (e.g., writer.GetMemory(128 * 1024)) and use ReadOnlySequence<byte>.Slice with SequencePosition next = readerBuffer.GetPosition(1, eol) to avoid re-computing offsets.
Impact: On modern SSDs and high-latency network streams, these changes reduce kernel calls by ~50%, allow arbitrarily long NMEA sentences, and provide accurate telemetry for adaptive throttling.
3. Sentence parsing (NmeaLineParser.cs)
- Static ASCII sentinels (
VdmAscii, VdoAscii) are stored as byte[] built via Encoding.ASCII, incurring a static constructor and GC pinning. Using private static ReadOnlySpan<byte> VdmAscii => "VDM"u8; avoids both.
TagBlock property (line 235) allocates a new NmeaTagBlockParser on every access. In NmeaLineToAisStreamAdapter we access it twice per line (lines 94‑105), meaning duplicate parsing and checksum validation.
GetSingleDigitField (lines 249‑273) rejects multi-digit fragment counts. The AIS spec allows up to 9, but third-party data sets often emit 10+, causing avoidable exceptions and re-parses.
- The parser repeatedly calls
remainingFields.IndexOf((byte)',')), rescanning the same span. Using SearchValues<byte> or Utf8Parser over a SequenceReader<byte> can reduce branch mispredictions and take advantage of AVX2.
Recommendations:
- Cache a
NmeaTagBlockParser inside NmeaLineParser (e.g., private readonly NmeaTagBlockParser? tagBlock;) so consumers do not re-parse.
- Replace digit parsing with
Utf8Parser.TryParse on ReadOnlySpan<byte> to accept multi-digit counts while remaining allocation-free.
- Adopt
SearchValues<byte> for delimiter scans so the JIT can vectorize them automatically.
Impact: Eliminates redundant work during fragment-heavy loads and removes latent incompatibilities with newer AIS feeds.
4. Fragment reassembly (NmeaLineToAisStreamAdapter.cs)
Issues:
- Lines 89‑187: Each fragment copies the whole NMEA line into a pooled array and creates
NmeaLineParser instances multiple times—once when first received, again while summing payload lengths, and again during reassembly.
- Lines 196‑236:
fragmentGroupIdsToRemove uses stackalloc int[this.messageFragments.Count]. Under high traffic, the dictionary can reach thousands of entries and blow the stack (especially on macOS where the guard page is small).
- Dictionary usage does two lookups (
TryGetValue / Add). .NET 8 introduced CollectionsMarshal.GetValueRefOrAddDefault, which can mutate entries in-place without allocations.
parsedLine.TagBlock is read twice (lines 92‑101), recreating the parser each time (see §3).
- When discarding aged fragments (lines 208‑236), the code finds the “last non-null entry” by looping backward and assumes at least one fragment exists, which throws if the group never received any payload. Aside from correctness, the work re-parses the fragment again to produce error text.
Recommendations:
- Store only payload slices: rent buffers sized to
parsedLine.Payload.Length, copy just payload bytes, and track padding per fragment. When all fragments arrive, stitch payload spans via IBufferWriter<byte> without re-parsing.
- Use
CollectionsMarshal.GetValueRefOrAddDefault (net8+) to fetch or create FragmentedMessage without double hashing. For netstandard builds, leave the current path via #if.
- Replace
stackalloc removal list with ArrayPool<int>.Shared.Rent(Math.Min(messageFragments.Count, 1024)) or a reusable ValueListBuilder<int>.
- Cache
NmeaTagBlockParser up-stack so we do not rehydrate it per property access.
- Add diagnostic counters for dropped fragments so operations teams can react before stacks build up.
Impact: Cuts fragment reassembly CPU cost roughly in half, mitigates high-memory workloads, and prevents stack overflows during fragment storms.
5. Bit-vector & payload decoding (NmeaAisBitVectorParser.cs, NmeaPayloadParser.cs, AisStrings.cs)
NmeaPayloadParser.AisAsciiTo6Bits (lines 37‑45) performs nested ternary checks and allocates exception strings on every invalid byte. Modern runtimes provide ArgumentOutOfRangeException.ThrowIfNegative and ThrowHelper to keep the happy path branchless.
GetUnsignedInteger (lines 67‑138) processes at most 6 bits per loop iteration and calls AisAsciiTo6Bits for every chunk. For a 256-bit payload, this means ~43 calls even if later fields re-read the same 6-bit value.
GetSignedInteger (lines 43‑59) recomputes sign masks; we can sign-extend via bit shifts (int shift = 32 - (int)bitCount; return ((int)value << shift) >> shift;).
- No attempt is made to leverage vectorization; each ASCII byte is decoded independently. A lookup table (
ReadOnlySpan<byte> Lookup = ...) or even Vector128<byte> comparisons can translate 16 bytes at a time.
Recommendations:
- During construction, decode the payload once into a
Span<uint> (6-bit values) stored either on the stack (for short payloads) or in a pooled buffer. Keep BitCount and re-use the decoded span in all Get* calls.
- Expose APIs that operate over
BitReader semantics (e.g., TryReadUInt32(bitCount, out uint value) returning a ref struct), enabling consumers to sequentially advance without recomputing offsets.
- Use source-generated throw helpers (or
ArgumentOutOfRangeException.ThrowIfGreaterThan) for invalid ASCII to keep the hot path branch-free.
- Consider a
static ReadOnlySpan<byte> SixBitLookup => "................................0123456789:;<=>?@ABCDEFGHIJKLMNO...................PQRSTUVWXYZ[\\]^_abcdefghijklmno...................pqrstuvwxyz{|}~"u8;` to map ASCII directly.
Impact: Reduces per-field latency, enabling real-time decoding of high-density AIS data while preserving zero-allocation guarantees.
6. Downstream processors (ReadAllPositions.cs, InspectMessageType.cs)
- Each processor repeatedly calls
NmeaPayloadParser.PeekMessageType, which re-validates the payload and decodes the first 6 bits for every consumer. Consider caching the type when the message is first parsed and passing it alongside the payload.
ReadAllPositions constructs new parser structs for each message type. With multi-million message streams, that’s a lot of redundant work when consumers only need a subset of fields. Introducing specialized fast-paths (e.g., “just read lat/long + speed”) would reduce instruction count.
Recommendations: Add a lightweight metadata struct (message type, padding, predecoded NmeaAisBitVectorParser) that can be passed to processors, and expose APIs for reading subsets of fields without rebuilding parser state.
7. Benchmarks & diagnostics (Ais.Net.Benchmarks)
GlobalSetup rewrites a 1 M line file synchronously (File.ReadAllLines + StreamWriter) every run, introducing noise and blocking the thread pool during CI. Persist the generated data as part of the repo or use memory-mapped files so that the dataset is materialized once.
- Benchmarks currently report only average time. Enable BenchmarkDotNet’s
HardwareCounters (instructions, cache misses) and EventPipeProfiler to capture GC pressure.
- Add scenarios for concurrent parsing (e.g., multiple
NmeaStreamParser.ParseStreamAsync tasks) and native AOT builds so regressions show up before shipping.
Impact: More reliable perf baselines and richer telemetry whenever parser changes are introduced.
8. Observability & correctness
NmeaLineToAisStreamAdapter.FreeRentedBuffers writes to Console, which is problematic in services. Replace with structured logging/event counters so operators can monitor fragment drops without stdout parsing.
- Add
System.Diagnostics.Metrics counters (lines processed, fragments dropped, average message latency) to feed OpenTelemetry. Modern .NET makes this zero-cost when no listener is attached.
By implementing the above, the Solutions codebase can fully leverage the capabilities of .NET 10/C# 14, minimize allocations, and keep throughput competitive on modern CPU architectures while retaining compatibility with older deployments.
Opus 4.5 inside Claude Code
Ais.Net Performance Improvements for .NET 10 / C# 14
Executive Summary
This document outlines performance improvements for the Ais.Net library, targeting the transition from .NET Standard 2.0/2.1 to .NET 10 and C# 14. The library is already well-optimized with zero-allocation parsing using ref struct and ReadOnlySpan<byte>. The improvements below leverage 6+ years of .NET runtime advancements, modern CPU intrinsics, and new language features to further enhance throughput for mission-critical AIS message processing.
Priority Classification:
- P0 (Critical): Immediate measurable throughput gains
- P1 (High): Significant improvements with moderate effort
- P2 (Medium): Good improvements, consider for next iteration
- P3 (Low): Minor optimizations or future considerations
1. Target Framework Modernization
Current State
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
Recommendation (P0)
Add modern TFMs while maintaining backwards compatibility:
<TargetFrameworks>net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0</TargetFrameworks>
Rationale:
- Unlocks all optimizations below via
#if NET8_0_OR_GREATER directives
- .NET 8+ has significantly improved JIT codegen, especially for Span operations
- Allows use of hardware intrinsics, SearchValues, FrozenDictionary, etc.
- Users on modern runtimes get automatic performance gains
2. SIMD/Vectorization Opportunities
2.1 AIS ASCII to 6-Bit Conversion (P0)
File: NmeaPayloadParser.cs:37-45
Current Implementation:
internal static byte AisAsciiTo6Bits(byte c) => (byte)(c < 48
? throw new ArgumentOutOfRangeException(...)
: (c < 88
? c - 48
: (c < 96
? throw new ArgumentOutOfRangeException(...)
: (c < 120
? c - 56
: throw new ArgumentOutOfRangeException(...)))));
Proposed Improvement:
#if NET8_0_OR_GREATER
// Lookup table approach - branch-free, cache-friendly
private static ReadOnlySpan<byte> AisDecodeLut => new byte[128]
{
// Pre-computed lookup: invalid = 0xFF, valid = decoded value
// Indices 48-87: value - 48
// Indices 96-119: value - 56
// All others: 0xFF (invalid)
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static byte AisAsciiTo6Bits(byte c)
{
byte result = c < 128 ? AisDecodeLut[c] : (byte)0xFF;
if (result == 0xFF)
ThrowInvalidPayloadCharacter(c);
return result;
}
// Vectorized batch conversion for SIMD-enabled paths
internal static void AisAsciiTo6BitsBatch(ReadOnlySpan<byte> source, Span<byte> dest)
{
if (Vector256.IsHardwareAccelerated && source.Length >= Vector256<byte>.Count)
{
// Process 32 bytes at a time using AVX2
// Range check and convert in parallel
}
// Scalar fallback
}
#endif
Expected Impact: 2-4x throughput for payload decoding on AVX2-capable hardware.
2.2 Bit Vector Extraction (P1)
File: NmeaAisBitVectorParser.cs:67-138
Current Implementation:
The GetUnsignedInteger method processes bits character-by-character in a loop.
Proposed Improvements:
- Use
BitOperations class (.NET 5+)
#if NET5_0_OR_GREATER
using System.Numerics;
// Replace manual bit manipulation with hardware-accelerated operations
int leadingZeros = BitOperations.LeadingZeroCount(value);
int trailingZeros = BitOperations.TrailingZeroCount(value);
uint rotated = BitOperations.RotateLeft(value, shift);
#endif
- Pre-decode entire payload to 6-bit values
// For messages accessed multiple times, pre-decode once
Span<byte> decoded = stackalloc byte[ascii.Length];
AisAsciiTo6BitsBatch(ascii, decoded);
// Then extract bits from contiguous 6-bit values
- Optimized extraction for common field sizes (6, 8, 9, 10, 12, 27, 28, 30 bits)
// Specialized fast paths for frequently-used bit widths
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint GetUnsigned6Bits(uint bitOffset)
{
// Single character extraction - no loop needed
int charOffset = (int)(bitOffset / 6);
int localOffset = (int)(bitOffset % 6);
if (localOffset == 0)
return NmeaPayloadParser.AisAsciiTo6Bits(this.ascii[charOffset]);
// Handle cross-character case
}
Expected Impact: 20-40% reduction in per-field extraction time.
2.3 SearchValues for Delimiter Scanning (P0)
File: NmeaLineParser.cs, NmeaTagBlockParser.cs, NmeaStreamParser.cs
Current Implementation:
int nextComma = remainingFields.IndexOf((byte)',');
int tagBlockEndIndex = line.Slice(1).IndexOf(TagBlockMarker);
Proposed Improvement (.NET 8+):
#if NET8_0_OR_GREATER
private static readonly SearchValues<byte> Delimiters = SearchValues.Create(","u8);
private static readonly SearchValues<byte> LineEndOrTagBlock = SearchValues.Create("\n\r\\"u8);
// Single scan for multiple delimiters
int delimiterIndex = span.IndexOfAny(LineEndOrTagBlock);
#endif
Expected Impact: 15-30% faster delimiter scanning, especially for long lines.
3. Memory Allocation Elimination
3.1 UTF-8 String Literals (P0)
File: NmeaLineParser.cs:17-18
Current Implementation:
private static readonly byte[] VdmAscii = Encoding.ASCII.GetBytes("VDM");
private static readonly byte[] VdoAscii = Encoding.ASCII.GetBytes("VDO");
Proposed Improvement (C# 11+):
#if NET7_0_OR_GREATER
private static ReadOnlySpan<byte> VdmAscii => "VDM"u8;
private static ReadOnlySpan<byte> VdoAscii => "VDO"u8;
#else
private static readonly byte[] VdmAscii = Encoding.ASCII.GetBytes("VDM");
private static readonly byte[] VdoAscii = Encoding.ASCII.GetBytes("VDO");
#endif
Rationale:
- Zero heap allocation - data stored directly in assembly
- Compiler-verified UTF-8 encoding
ReadOnlySpan<byte> returned from ROM
3.2 Stream Parser Buffer Pooling (P1)
File: NmeaStreamParser.cs:116
Current Implementation:
byte[] splitLineBuffer = new byte[1000];
Proposed Improvement:
#if NET6_0_OR_GREATER
// Use ArrayPool for the split line buffer
byte[] splitLineBuffer = ArrayPool<byte>.Shared.Rent(1024);
try
{
// ... processing
}
finally
{
ArrayPool<byte>.Shared.Return(splitLineBuffer);
}
#endif
Note: For very hot paths, consider stackalloc with a reasonable limit:
Span<byte> splitLineBuffer = stackalloc byte[1024];
3.3 FrozenDictionary for Fragment Tracking (P2)
File: NmeaLineToAisStreamAdapter.cs:21
Current Implementation:
private readonly Dictionary<int, FragmentedMessage> messageFragments = new Dictionary<int, FragmentedMessage>();
Proposed Improvement:
While fragments are dynamic, consider:
- Pre-sized Dictionary with expected capacity
- Alternative data structure for small counts (fragments are typically 1-9):
#if NET8_0_OR_GREATER
// For small fragment counts, linear search may be faster
private FragmentedMessage[] fragmentArray = new FragmentedMessage[10];
private int fragmentCount = 0;
#endif
3.4 ThrowHelper Pattern for Exception Hot Paths (P0)
Files: All parser files
Current Implementation:
throw new ArgumentOutOfRangeException("Payload characters must be in range 48-87 or 96-119");
throw new ArgumentException("Invalid data. Expected '!' at sentence start");
Proposed Improvement:
internal static class ThrowHelpers
{
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowInvalidPayloadCharacter()
=> throw new ArgumentOutOfRangeException("Payload characters must be in range 48-87 or 96-119");
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowInvalidSentenceStart()
=> throw new ArgumentException("Invalid data. Expected '!' at sentence start");
// ... other exception helpers
}
Rationale:
- Keeps hot path methods small (better inlining)
- Moves exception construction code out of hot path
[DoesNotReturn] helps JIT understand control flow
- Avoids string allocation until exception is actually thrown
4. Modern C# Language Features
4.1 Primary Constructors (C# 12+) (P3)
File: NmeaTagBlockSentenceGrouping.cs
Current:
public readonly struct NmeaTagBlockSentenceGrouping
{
public NmeaTagBlockSentenceGrouping(int sentenceNumber, int sentencesInGroup, int groupId)
{
this.SentenceNumber = sentenceNumber;
this.SentencesInGroup = sentencesInGroup;
this.GroupId = groupId;
}
public int GroupId { get; }
public int SentenceNumber { get; }
public int SentencesInGroup { get; }
}
Proposed:
public readonly struct NmeaTagBlockSentenceGrouping(int sentenceNumber, int sentencesInGroup, int groupId)
{
public int GroupId { get; } = groupId;
public int SentenceNumber { get; } = sentenceNumber;
public int SentencesInGroup { get; } = sentencesInGroup;
}
4.2 Collection Expressions (C# 12+) (P3)
File: NmeaLineParser.cs
Where applicable, replace array initializations:
// Current
private static readonly byte[] VdmAscii = Encoding.ASCII.GetBytes("VDM");
// With collection expressions (where not using u8 literals)
private static readonly byte[] SomeArray = [0x01, 0x02, 0x03];
4.3 Pattern Matching Improvements (P2)
File: NmeaLineParser.cs:90-118
Current (switch expression is good, but can be enhanced):
this.AisTalker = talkerFirstChar switch
{
(byte)'A' => talkerSecondChar switch { ... },
(byte)'B' => talkerSecondChar switch { ... },
// ...
};
Alternative using tuple patterns:
this.AisTalker = (talkerFirstChar, talkerSecondChar) switch
{
((byte)'A', (byte)'I') => TalkerId.MobileStation,
((byte)'A', (byte)'B') => TalkerId.BaseStation,
((byte)'A', (byte)'D') => TalkerId.DependentBaseStation,
// ... flattened structure, potentially better JIT optimization
_ => ThrowHelpers.ThrowUnrecognizedTalkerId<TalkerId>()
};
4.4 Required Members (C# 11+) (P3)
File: NmeaParserOptions.cs
#if NET7_0_OR_GREATER
public class NmeaParserOptions
{
public bool ThrowWhenTagBlockContainsUnknownFields { get; init; } = true;
public int MaximumUnmatchedFragmentAge { get; init; } = 8;
}
#endif
5. Async and I/O Improvements
5.1 High-Precision Timing (P1)
File: NmeaStreamParser.cs:109-110
Current:
int ticksAtStart = Environment.TickCount;
Proposed:
#if NET7_0_OR_GREATER
long ticksAtStart = Stopwatch.GetTimestamp();
// Later:
double elapsedMs = Stopwatch.GetElapsedTime(ticksAtStart).TotalMilliseconds;
#else
int ticksAtStart = Environment.TickCount;
#endif
Rationale: Stopwatch.GetTimestamp() provides nanosecond precision on modern systems.
5.2 IAsyncEnumerable Support (P2)
New API Addition:
#if NET8_0_OR_GREATER
public static async IAsyncEnumerable<NmeaLineParser> ParseLinesAsync(
Stream stream,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Yield parsed lines as they become available
// Allows consumers to use LINQ operators, filtering, etc.
}
#endif
5.3 RandomAccess for File I/O (P2)
File: NmeaStreamParser.cs:79
Current:
using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 1, useAsync: true);
Alternative for certain scenarios:
#if NET6_0_OR_GREATER
using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.Read, FileOptions.Asynchronous | FileOptions.SequentialScan);
// Use RandomAccess.ReadAsync for specific offsets
// Or continue with FileStream but with optimized options
#endif
5.4 ConfigureAwait Improvements (P3)
.NET 8+ consideration:
// For library code, consider adding ConfigureAwait(ConfigureAwaitOptions.ForceYielding)
// in specific scenarios where you want to ensure yielding
6. Bit Manipulation Optimizations
6.1 Branchless Minimum (P1)
File: NmeaAisBitVectorParser.cs:89
Current:
result <<= Math.Min(6, remainingBits);
Proposed:
// Branchless minimum for power-of-2 range
int shift = remainingBits & ~(remainingBits >> 31); // Handle negative (won't happen here)
shift = shift > 6 ? 6 : shift; // Compiler may optimize to cmov
Or use Math.Min and trust the JIT (modern .NET JIT often emits branchless code for simple Math.Min patterns).
6.2 Sign Extension Optimization (P1)
File: NmeaAisBitVectorParser.cs:43-58
Current:
public int GetSignedInteger(uint bitCount, uint bitOffset)
{
int result = (int)this.GetUnsignedInteger(bitCount, bitOffset);
int sbitCount = (int)bitCount;
int msb = 1 << (sbitCount - 1);
bool isNegative = (result & msb) != 0;
if (isNegative)
{
const int allOnesExceptLsb = -2;
int signBits = allOnesExceptLsb << (sbitCount - 1);
result |= signBits;
}
return result;
}
Proposed (branchless sign extension):
#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetSignedInteger(uint bitCount, uint bitOffset)
{
uint unsigned = this.GetUnsignedInteger(bitCount, bitOffset);
int shift = 32 - (int)bitCount;
// Arithmetic right shift propagates sign bit
return (int)(unsigned << shift) >> shift;
}
#endif
Expected Impact: Eliminates branch in sign extension, ~10-15% faster for signed integer fields (latitude, longitude, rate of turn).
7. Span and Memory Optimizations
7.1 SequenceReader for Buffer Parsing (P2)
File: NmeaStreamParser.cs ProcessBuffer method
Consideration:
#if NET6_0_OR_GREATER
var reader = new SequenceReader<byte>(remainingSequence);
while (reader.TryReadTo(out ReadOnlySpan<byte> line, (byte)'\n'))
{
// Process line
}
#endif
Note: Current implementation is already efficient; SequenceReader may add overhead for simple newline scanning.
7.2 Span Slicing in Loops (P2)
File: NmeaAisTextFieldParser.cs:67-73
Current:
public void WriteAsAscii(in Span<byte> targetBuffer)
{
for (int i = 0; i < targetBuffer.Length; ++i)
{
targetBuffer[i] = this.GetAscii((uint)i);
}
}
Proposed (batch processing):
public void WriteAsAscii(in Span<byte> targetBuffer)
{
uint charCount = this.CharacterCount;
for (uint i = 0; i < charCount; ++i)
{
uint bitIndexInField = i * 6;
byte aisValue = (byte)this.bits.GetUnsignedInteger(6, this.bitOffset + bitIndexInField);
targetBuffer[(int)i] = AisStrings.AisCharacterToAsciiValue(aisValue);
}
}
Further optimization: inline GetAscii logic to avoid per-character method call overhead.
8. JIT and Codegen Hints
8.1 Aggressive Inlining Attributes (P1)
Add [MethodImpl(MethodImplOptions.AggressiveInlining)] to:
AisAsciiTo6Bits
GetUnsignedInteger (for small bit counts)
GetBit
AisCharacterToAsciiValue
- Property getters on parser structs
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint MessageType => this.bits.GetUnsignedInteger(6, 0);
8.2 SkipLocalsInit (P2)
For hot allocation paths:
#if NET5_0_OR_GREATER
[SkipLocalsInit]
public static async Task ParseStreamAsync(...)
{
// stackalloc buffers won't be zero-initialized
}
#endif
Caution: Only use where you're certain all locals are written before read.
8.3 Suppress GC Transitions (P3)
For extremely hot paths with no managed allocations:
#if NET7_0_OR_GREATER
[SuppressGCTransition]
[UnmanagedCallersOnly]
// For native interop scenarios only
#endif
9. API Additions for Modern Consumers
9.1 Generic Math Support (P3)
File: New IAisPositionReport interface
#if NET7_0_OR_GREATER
public interface IAisPositionReport<TSelf> where TSelf : IAisPositionReport<TSelf>
{
static abstract int GetLatitude10000thMins(in TSelf report);
static abstract int GetLongitude10000thMins(in TSelf report);
}
#endif
9.2 Utf8JsonWriter Support (P2)
#if NET6_0_OR_GREATER
public static class AisJsonExtensions
{
public static void WriteAsJson(this NmeaAisPositionReportClassAParser parser, Utf8JsonWriter writer)
{
writer.WriteStartObject();
writer.WriteNumber("mmsi"u8, parser.Mmsi);
writer.WriteNumber("lat"u8, parser.Latitude10000thMins);
writer.WriteNumber("lon"u8, parser.Longitude10000thMins);
// ... zero-allocation JSON serialization
writer.WriteEndObject();
}
}
#endif
10. Benchmarking Recommendations
10.1 Update Benchmark Project
<TargetFrameworks>net10.0;net9.0;net8.0;net6.0</TargetFrameworks>
10.2 Key Benchmarks to Add
- Payload decoding throughput (bytes/second for AisAsciiTo6Bits)
- Bit extraction throughput (operations/second for GetUnsignedInteger)
- Line parsing throughput (lines/second for NmeaLineParser construction)
- Memory allocation profile (bytes allocated per message)
- Cold vs warm path performance (first message vs steady-state)
10.3 BenchmarkDotNet Configuration
[MemoryDiagnoser]
[DisassemblyDiagnoser(maxDepth: 3)]
[Config(typeof(MultiTfmConfig))]
public class ParsingBenchmarks
{
// Compare across TFMs
}
11. Implementation Priority Matrix
| Improvement |
Priority |
Effort |
Impact |
Breaking Change |
| Add net10.0 TFM |
P0 |
Low |
High |
No |
| UTF-8 string literals |
P0 |
Low |
Medium |
No |
| ThrowHelper pattern |
P0 |
Medium |
Medium |
No |
| SearchValues for delimiters |
P0 |
Medium |
High |
No |
| AisAsciiTo6Bits lookup table |
P0 |
Medium |
High |
No |
| Branchless sign extension |
P1 |
Low |
Medium |
No |
| AggressiveInlining attributes |
P1 |
Low |
Medium |
No |
| High-precision timing |
P1 |
Low |
Low |
No |
| Bit extraction fast paths |
P1 |
High |
High |
No |
| ArrayPool for buffers |
P1 |
Medium |
Medium |
No |
| SIMD batch conversion |
P1 |
High |
Very High |
No |
| IAsyncEnumerable API |
P2 |
Medium |
Medium |
No (additive) |
| SequenceReader usage |
P2 |
Medium |
Low |
No |
| Primary constructors |
P3 |
Low |
None |
No |
| Generic math interfaces |
P3 |
High |
Low |
No (additive) |
12. Migration Strategy
Phase 1: Foundation
- Add modern TFMs to project
- Implement
#if conditional compilation structure
- Add ThrowHelper pattern
- Add UTF-8 string literals
Phase 2: Core Optimizations
- Implement lookup table for AIS ASCII conversion
- Add SearchValues for delimiter scanning
- Implement branchless sign extension
- Add AggressiveInlining attributes
Phase 3: Advanced Optimizations
- Implement SIMD batch conversion
- Add specialized bit extraction paths
- Optimize text field parsing
- Profile and iterate
Phase 4: API Enhancements
- Add IAsyncEnumerable support
- Add Utf8JsonWriter extensions
- Consider source generators for parser boilerplate
13. Risks and Mitigations
| Risk |
Mitigation |
| Regression in netstandard2.0 |
Maintain separate code paths with #if |
| SIMD code complexity |
Thoroughly benchmark; fall back to scalar |
| Increased binary size |
Consider separate packages for modern TFMs |
| Behavioral changes |
Comprehensive test coverage before/after |
14. Conclusion
The Ais.Net library has an excellent foundation with its zero-allocation ref struct design. The improvements outlined here can provide:
- 15-30% improvement from SearchValues and UTF-8 literals
- 20-40% improvement from optimized bit extraction
- 2-4x improvement in payload decoding with SIMD (AVX2 hardware)
- Better code maintainability with modern C# features
These improvements maintain full backwards compatibility through conditional compilation while giving users on modern .NET runtimes significant performance gains for their mission-critical AIS processing workloads.
Document Version: 1.0
Analysis Date: December 2024
Target: .NET 10 / C# 14
A colleague asked several AIs what we might do to improve performance. I've dumped the results here. We should work through these to see which suggestions look worthwhile, and create separate work items if appropriate:
Gemini 3 Pro
Performance Improvements for Ais.Net
This document outlines suggested performance improvements for the
Ais.Netlibrary, targeting .NET 10 and C# 14. The goal is to achieve zero-allocation, high-performance code suitable for modern CPU architectures.1. Modern .NET & C# Features
1.1. UTF-8 String Literals
Current:
Encoding.ASCII.GetBytes("VDM")creates a byte array at runtime (or static init).Recommendation: Use C# 11 UTF-8 string literals (
"VDM"u8). This compiles directly to aReadOnlySpan<byte>pointing to the data section of the assembly, avoiding array allocation and initialization overhead.1.2.
SequenceReader<T>for ParsingCurrent:
NmeaLineParserandNmeaTagBlockParsermanually slice spans and search for delimiters.Recommendation: Use
System.Buffers.SequenceReader<byte>. It provides optimized methods for reading primitives, advancing, and searching, often using SIMD under the hood. It also handlesReadOnlySequence<byte>natively, which is crucial for fragmented data (see 2.1).1.3.
SkipLocalsInitCurrent: Default zero-initialization of locals.
Recommendation: Apply
[SkipLocalsInit]to performance-critical methods (likeNmeaAisBitVectorParser.GetUnsignedInteger) to avoid the cost of zeroing stack memory, especially when usingstackalloc.1.4. SIMD Vectorization
Current: Scalar processing of bits and bytes.
Recommendation:
Vector128<byte>orVector256<byte>(viaVector128.Create((byte)',')) to find delimiters inNmeaLineParserin a single pass, rather than repeatedIndexOfcalls.vpmultishiftqbor shuffle) can sometimes perform parallel bit extraction and packing.2. Memory Management & Allocations
2.1. Zero-Copy Reassembly of Fragmented Messages
Current:
NmeaLineToAisStreamAdapterallocates a new buffer for each fragment, copies data into it, then allocates a larger buffer to combine them, and copies again.Recommendation:
ReadOnlyMemory<byte>(orReadOnlySequence<byte>segments).ReadOnlySequence<byte>representing the logical contiguous payload.NmeaAisBitVectorParserandNmeaPayloadParserto acceptReadOnlySequence<byte>(or useSequenceReader).2.2. Eliminate
Dictionaryin Hot PathCurrent:
NmeaLineToAisStreamAdapterusesDictionary<int, FragmentedMessage>to track fragments.Recommendation:
FrozenDictionary(if static) or a specialized high-performance map (likeSwissTableimplementation or open addressing) to reduce overhead.FragmentedMessagecontainers would reduce GC pressure.2.3. Fix Repeated Parsing in
NmeaLineParser.TagBlockCurrent: Accessing
parsedLine.TagBlockcreates a newNmeaTagBlockParserwhich immediately parses the entire tag block.NmeaLineToAisStreamAdapteraccesses this property multiple times per line, causing redundant parsing.Recommendation:
NmeaTagBlockParserlazy and lightweight (only parse when specific fields are requested), though this is harder withref struct.NmeaTagBlockParserinstance around.2.4. Lookup Table for 6-bit Decoding
Current:
NmeaPayloadParser.AisAsciiTo6Bitsuses nested ternary operators and branches.Recommendation: Use a static
ReadOnlySpan<byte>lookup table (size 128). This makes decoding branchless and extremely fast (O(1) memory access).3. API & Architecture
3.1. Exception Handling
Current:
NmeaLineParserandNmeaPayloadParserthrow exceptions (ArgumentException,NotSupportedException) for invalid data.Recommendation:
TryCreateorTryParsepattern returning aboolor a status enum (OperationStatus).3.2.
System.IO.PipelinesOptimizationCurrent:
NmeaStreamParsermanually creates aPipeand a background task to copyStreamtoPipe.Recommendation:
PipeReader.Create(Stream)which is optimized and handles buffering/copying efficiently.ProcessFileAsyncloop.3.3.
NmeaLineToAisStreamAdapterRedundant ParsingCurrent: When reassembling,
NmeaLineToAisStreamAdaptercreates a newNmeaLineParserfor each fragment to extract the payload.Recommendation:
FragmentedMessage.4. Micro-Optimizations
GetSingleDigitField: Replace manual char math withu8subtraction orUtf8Parser.Math.Min: InNmeaAisBitVectorParser,Math.Minmight add a branch. Bitwise operations can sometimes replace it.stackalloc: Ensurestackallocis used for small temporary buffers (like the fragment removal list) to avoid GC pressure.MethodImplOptions.AggressiveInlining: Apply to small hot methods likeAisAsciiTo6Bits(if not using lookup table) or bit extraction helpers.5. Benchmarking Strategy
Before applying these changes:
Solutions/Ais.Net.Benchmarks.NmeaLineParserconstructor (parsing speed).NmeaAisBitVectorParser.GetUnsignedInteger(decoding speed).NmeaLineToAisStreamAdapter(fragment reassembly overhead).dotnet-traceordotMemoryto confirm allocation hotspots.gpt5.1
Ais.Net Solutions Performance Review (2025)
This document records performance-focused observations and recommendations from a review of the
Solutionstree as of 2025-12-09, with a particular focus on the hot parsing path inAis.Net.The existing codebase already uses modern low-allocation techniques (e.g.,
ref struct,Span<T>,ArrayPool<T>,System.IO.Pipelines); most suggestions below are incremental, targeted optimizations or opportunities to take advantage of .NET 8–10 and C# 12+.1. Target frameworks and runtime feature usage
1.1 Drop legacy
netstandard2.0-only paths and unify on modern runtimesFiles:
Ais.Net/Ais/Net/NmeaStreamParser.cs(#if NETSTANDARD2_0block inCreateFileReader)Observation
CreateFileReaderstill carries a#if NETSTANDARD2_0branch that usesMemoryMarshal.TryGetArrayandStream.ReadAsync(byte[], int, int); the non-NETSTANDARD2_0path already usesStream.ReadAsync(Memory<byte>).netstandard2.0build; it prevents usage of newer BCL and JIT features that assume a modern baseline.Recommendation
netstandard2.0(and ideallynetstandard2.1) and target only modern TFMs (e.g.,net8.0,net9.0,net10.0) forAis.Net.#if NETSTANDARD2_0branch inCreateFileReaderand always useReadAsync(Memory<byte>)with the newerFileStreamimplementation; this unlocks additional JIT and I/O optimizations available only in current runtimes and simplifies the code.Impact
FileStreamOptions,RandomAccess,Stream.ReadExactlyAsync).2. NMEA stream ingestion (
NmeaStreamParser)File:
Ais.Net/Ais/Net/NmeaStreamParser.cs2.1 File open and I/O strategy
Observation
ParseFileAsync(string, INmeaLineStreamProcessor, NmeaParserOptions)opens the file with:Pipe-based reader inCreateFileReader.1disablesFileStream's internal buffering, forcing every call to the custom reader to result in a kernel I/O call, shifting all buffering into the customPipe.Recommendations
Pipeimplementation inCreateFileReadernow that .NET ships an optimizedPipeReader.Create(Stream, StreamPipeReaderOptions)implementation:CreateFileReaderwithPipeReader reader = PipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: N, minimumReadSize: M));and remove thePipe+ProcessFileAsyncscaffolding.bufferSize/minimumReadSize(e.g., 64–256 KiB) based on real-world log sizes; this removes the 1 MiB segment size guess and automatically tracks runtime tuning.Pipe, consider:FileStreambuffer size (e.g., 64 KiB) instead of1, and profiling whether the combination ofFileStreambuffering +Pipeis faster than forcing all buffering into thePipealone.writer.GetMemory(desiredSize)with a non-trivialdesiredSize(e.g., 64 KiB) to reduce the number of small writes into thePipe.Impact
2.2 Line splitting loop
Observation
ParseStreamAsyncprocesses thePipeReaderbuffer by scanning for\nusingReadOnlySequence<byte>.PositionOf((byte)'\n')and manually reassembling multi-segment lines into a fixedsplitLineBufferof size 1000.IsSingleSegment,lineSpan = line.First.Span;is zero-allocation; for multi-segment lines, a copy intosplitLineBufferis performed per line.Recommendations
splitLineBufferupper bound is safe for all expected NMEA line sources; if longer lines are possible, consider:ArrayPool<byte>-backed buffer sized to the longest observed line; orArrayPool<byte>.Shared.Rent()/Return()pattern for rare long lines, keeping the stack-allocated or fixed 1 KiB path for the majority.System.Buffers.SequenceReader<byte>(available in modern runtimes) which offers optimized line-oriented reading utilities and can reduce manualPositionOf/slice juggling.Impact
2.3 Progress accounting
Observation
Environment.TickCountand reports everyLineCountInterval = 100000lines; the counters and time calculations are 32-bit integer-based.Recommendations
Stopwatch.GetTimestamp()for higher-resolution wall-clock measurement and immunity toTickCountwraparound.Impact
3. Sentence parsing (
NmeaLineParser)File:
Ais.Net/Ais/Net/NmeaLineParser.cs3.1 Character classification
Observation
char.IsDigit(char)is globalization-aware and more expensive than necessary when the input is guaranteed ASCII.Recommendation
char.IsAsciiDigit((char)remainingFields[nextComma + 1]), which is explicitly optimized for ASCII input and avoids globalization costs.Impact
3.2 Tag block parsing reuse
Observation
TagBlockis implemented as:NmeaLineToAisStreamAdapter.OnNextthe property is accessed multiple times per line when tag blocks are present.Recommendation
NmeaTagBlockParserinsideNmeaLineParser, constructed lazily on first access:bool tagBlockParsed;+NmeaTagBlockParser tagBlock;inside theref structand only parse once whenTagBlockis accessed.NmeaTagBlockParseris aref struct, it can be stored as a field in anotherref structwithout heap allocation.Impact
TagBlockon the same parsed line.3.3 Minor branch and slicing optimizations
Observations & Suggestions
this.Sentence.Slice(3, 3).SequenceEqual(VdmAscii)and the corresponding VDO check perform twoSliceoperations per line; for very high message rates you could:Sentence[3],Sentence[4],Sentence[5]to'V','D','M'/'O'to avoid the extra span creation andSequenceEqualcall.GetSingleDigitFieldassumes fields are a single digit or empty; if the protocol may evolve to multi-digit counts, consider adding a non-throwingTryGetIntFieldthat handles multi-digit ASCII integers viaUtf8Parser, mirroring the approach inNmeaTagBlockParser.Impact
4. Fragment reassembly (
NmeaLineToAisStreamAdapter)File:
Ais.Net/Ais/Net/NmeaLineToAisStreamAdapter.cs4.1 Redundant re-parsing of fragments
Observation
byte[]buffers.totalPayloadSize, each fragment buffer is wrapped in a freshNmeaLineParsermultiple times:Recommendation
byte[]inFragmentedMessageto avoid repeated parsing:NmeaLineParserand storePayloadOffset,PayloadLength, andPadding(or theReadOnlySpan<byte>slice indices) in a small value type.allFragmentsReceivedcheck and the reassembly copy instead of re-parsing.Impact
4.2 Group expiry and stack allocation size
Observation
OnNextbuilds aSpan<int> fragmentGroupIdsToRemove = stackalloc int[this.messageFragments.Count];when there are outstanding fragments.messageFragments.Countcould become large; a largestackalloc(thousands ofints) increases stack pressure and risks stack overflow in extreme cases.Recommendation
stackallocwith anArrayPool<int>.Shared-backed buffer, or use a small, fixed-sizestackalloc(e.g., 64/128) and fall back to a pooled array when more capacity is needed.lineNumberwhen eachFragmentedMessagewas last updated and periodically scan only a bounded number of entries per call to amortize work.Impact
4.3 Logging and cleanup allocations
Observation
FreeRentedBuffersusesthis.messageFragments.Keys.ToArray()and writes directly toConsolefor each group with missing fragments.Recommendations
Keys.ToArray()with anArrayPool<int>.Shared-backed buffer and manual enumeration; orConsole.WriteLine.Impact
4.4 Error handling strategy for noisy inputs
Observation
ArgumentException(e.g., duplicate sentence in group, non-zero padding in non-final fragments), which surface viaOnError.Recommendation
Try*parse paths for expected error patterns (e.g.,TryHandleFragmentreturning a status enum) and use error codes instead of exceptions for recoverable invalid inputs.Impact
5. Bit-vector decoding (
NmeaAisBitVectorParserand friends)File:
Ais.Net/Ais/Net/NmeaAisBitVectorParser.cs5.1 Bit extraction loop
Observation
GetUnsignedIntegeriteratively shiftsresultleft and ANDs/masks out bits from each 6-bit AIS character, handling arbitrary bit offsets and lengths up to 32 bits.Recommendations
GetUnsignedIntegerunder realistic workloads using BenchmarkDotNet targeting modern runtimes; if it dominates CPU time:nint/nuintfor indices and shifts to better match JIT expectations on 64-bit hardware.System.Runtime.IntrinsicsorVector128/Vector256to decode multiple 6-bit characters in parallel when reading fields that span multiple AIS characters.Impact
5.2 Exception messages and range checks
Observation
AisAsciiTo6Bitsuses nested conditional operators and throws with repeated string literals for invalid ranges.GetUnsignedIntegerandGetSignedIntegerthrowArgumentOutOfRangeExceptionfor off-end or oversized fields.Recommendations
TryAisAsciiTo6Bits/TryGetUnsignedIntegervariants returningbool+outvalue, and use them in hot paths that can tolerate skipping invalid messages viaOnError.Impact
6. Text field parsing (
NmeaAisTextFieldParser,AisStrings)Files:
Ais.Net/Ais/Net/NmeaAisTextFieldParser.csAis.Net/Ais/Net/AisStrings.cs6.1 Buffer sizing and bounds
Observation
WriteAsAscii(in Span<byte> targetBuffer)assumestargetBuffer.Lengthis at leastCharacterCountand writes exactlytargetBuffer.Lengthbytes.Recommendation
targetBuffer.Length == CharacterCountin the primary usage pattern; if you plan to reuse larger buffers, change the loop to:IBufferWriter<byte>or returns aReadOnlySpan<byte>view over a caller-provided buffer to better integrate with modern pipeline-based consumers.Impact
6.2 Character conversion table
Observation
AisStrings.AisCharacterToAsciiValuetranslates AIS 6-bit characters to ASCII with a simple conditional; this is already very efficient.Recommendation
ReadOnlySpan<byte>and index into it, but this is unlikely to be measurably faster than the current arithmetic.Impact
7. Tag block parsing (
NmeaTagBlockParser)File:
Ais.Net/Ais/Net/NmeaTagBlockParser.cs7.1 Parsing helpers and
Utf8ParserusageObservation
Utf8Parser.TryParseoverReadOnlySpan<byte>, with careful delimiter handling viaGetEnd.scopedparameters for spans in helpers likeParseSentenceGrouping, which is already a modern pattern.Recommendations
Utf8Parser/Utf8-based APIs overEncoding-based conversions.Impact
8. Error handling and API shape
8.1 Exception-heavy hot paths
Observation
NmeaLineParser,NmeaTagBlockParser, and the message-specific parsers, invalid input is typically handled by throwingArgumentException/ArgumentOutOfRangeException/NotSupportedException.Recommendations
bool TryParseLine(ReadOnlySpan<byte> line, out NmeaLineParser parser, out ParseError error)bool TryParseTagBlock(ReadOnlySpan<byte> source, out NmeaTagBlockParser parser, out TagBlockError error)Try*methods in high-throughput ingestion code and reserve exception-throwing variants for public, developer-facing APIs where misuse should be clearly signaled.Impact
9. General .NET 8–10 opportunities
9.1 Hardware and JIT-friendly coding patterns
Recommendations
nint/nuintandnint-based index arithmetic in inner loops (e.g.,NmeaAisBitVectorParser) to play well with the 64-bit JIT.[MethodImpl(MethodImplOptions.AggressiveInlining)]where benchmarks show non-trivial benefit (e.g., smallGetUnsignedIntegerhelpers).9.2 Benchmarking and profiling
Recommendations
Ais.Net.Benchmarksto cover:NmeaStreamParser.ParseStreamAsyncend-to-end with realistic sample data from production environments.NmeaAisBitVectorParser.GetUnsignedInteger,NmeaLineParserconstruction, and fragmented message reassembly.Impact
10. Summary
Ais.Netlibrary already uses a highly efficient, low-allocation design that is well-suited to high-throughput AIS parsing.Pipe/I/O strategy, (3) reducing redundant parsing and exception use on hot paths, and (4) benchmarkingNmeaAisBitVectorParserand fragmentation handling under real workloads.Opus 4.5
Solutions Performance Improvement Review (December 2025)
This document captures a targeted performance review of the code under
Solutions, with concrete modernization actions to keep the pipeline “zero allocation” friendly on .NET 10/C# 14 era runtimes. The review prioritizes hot paths (stream ingestion, parsing, fragment reassembly) and supporting assets (benchmarks, build configuration).Quick wins (highest ROI first)
Ais.Net/Ais.Net.csprojlines 14‑24;Common.*.projnetstandard2.x, pulling inSystem.IO.Pipelines4.7.5 andSystem.Memoryshims. Modern JIT/SIMD,CollectionsMarshal,ValueStringBuilder, NativeAOT, etc. are unavailable.net10.0;net8.0;netstandard2.1;netstandard2.0and conditionally light up modern intrinsics (#if NET8_0_OR_GREATER). Drop legacy package references where the runtime already provides them.Ais.Net/Ais/Net/NmeaStreamParser.cslines 108‑255Pipebridge reads withFileStreambufferSize 1,Environment.TickCount, and a fixed 1 kBsplitLineBuffer. This causes excess syscalls, bounded line lengths, and extra parsing passes for multi-segment sequences.FileStreamOptions(async + SequentialScan) plusPipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: 4 MB, minimumReadSize: 128 kB)). ReplacesplitLineBufferwith pooled/stack spans sized to actual line length, and switch timing toStopwatch.GetTimestamp()/ValueStopwatch.Ais.Net/Ais/Net/NmeaLineToAisStreamAdapter.cslines 81‑238NmeaTagBlockParserper line, store only payload slices + padding, and replace dictionary logic withCollectionsMarshal.GetValueRefOrAddDefault. Use pooledValueListBuilder<int>(orArrayPool<int>) for aged fragment tracking.Ais.Net/Ais/Net/NmeaAisBitVectorParser.cslines 26‑147;NmeaPayloadParser.cslines 22‑45Span<uint>of 6-bit values (usingstackallocup to ~256 B, elseArrayPool). UseBinaryPrimitives.ReadUInt32BigEndian/BitOperations.RotateLeftto pull up to 32 bits at once and implement sign-extension via arithmetic shifts.Ais.Net.Benchmarks/*.csEventPipeProfiler/HardwareCounters. Add scenarios that exercise multi-threaded ingestion and NativeAOT builds.Detailed findings
1. Target frameworks, analyzers, and packages
Solutions/Ais.Net/Ais.Net.csproj(lines 14‑24) still targets onlynetstandard2.0/2.1, forcing the inclusion ofSystem.IO.Pipelines4.7.5 andSystem.Memoryfor legacy TFMs. This blocks the use of modern APIs such asCollectionsMarshal,System.Buffers.SearchValues,Span<T>-friendly regexes,RandomAccess.ReadAsync, orNativeAOT-compatible trimming hints.net10.0;net8.0alongside the existing netstandard TFMs, and use<RuntimeIdentifier>-specificPublishAotprofiles for deployments where latency matters.Common.Net.projonce per-SDK analyzers (e.g., CA.Aot) are enabled so we can enable new warnings only where supported.System.IO.PipelinesNuGet dependency with the in-box version for modern TFMs, while keeping the package reference only for netstandard2.0.Impact: The compiler can now emit intrinsics (AVX‑512 on Linux, ARM64 SVE), and we reduce package restore time and dependency graph complexity.
2. Streaming and I/O hot path (
NmeaStreamParser.cs)Observed issues:
ParseStreamAsyncpollsPipeReader.ReadAsyncwithoutReadAtLeastAsync, resulting in tiny reads on slow disks and extra syscalls. Also, the manualProcessBufferrepeatedly scans for\nusingReadOnlySequence.PositionOf, which walks the sequence twice.new FileStream(path, … bufferSize: 1, useAsync: true)disables user-mode buffering but increases kernel transitions. Modern .NET exposesFileStreamOptionsthat preserve zero-copy semantics without artificially shrinking the buffer.byte[] splitLineBuffer = new byte[1000];is allocated per call and fails on >1 kB sentences, requiring reallocation (and silently truncating whenline.Length > 1000).Environment.TickCount, which wraps every 49 days and lacks resolution; progress reports will become negative on busy daemons.PipeWriterloop callswriter.GetMemory()with unspecified size, leading to 4 kB acquisitions on most platforms and inflated flush frequency.Recommendations:
PipeReader.Create(stream, new StreamPipeReaderOptions(bufferSize: 4 * 1024 * 1024, minimumReadSize: 128 * 1024, leaveOpen: true))and useSequenceReader<byte>to parse lines without copying, drastically reducing branch mispredictions.FileStreamOptions(async + SequentialScan) orRandomAccess.ReadAsyncto batch filesystem IO while still avoiding double-buffering.splitLineBufferwithSpan<byte> lineBuffer = line.IsSingleSegment ? line.FirstSpan : ArrayPool<byte>.Shared.Rent((int)line.Length)plustry/finallyreturn, or adoptValueListBuilder<byte>to keep short lines on the stack.ValueStopwatch/Stopwatch.GetTimestamp()and compute throughput viaStopwatch.Frequency, eliminating wrap-around bugs.PipeWriter(e.g.,writer.GetMemory(128 * 1024)) and useReadOnlySequence<byte>.SlicewithSequencePosition next = readerBuffer.GetPosition(1, eol)to avoid re-computing offsets.Impact: On modern SSDs and high-latency network streams, these changes reduce kernel calls by ~50%, allow arbitrarily long NMEA sentences, and provide accurate telemetry for adaptive throttling.
3. Sentence parsing (
NmeaLineParser.cs)VdmAscii,VdoAscii) are stored asbyte[]built viaEncoding.ASCII, incurring a static constructor and GC pinning. Usingprivate static ReadOnlySpan<byte> VdmAscii => "VDM"u8;avoids both.TagBlockproperty (line 235) allocates a newNmeaTagBlockParseron every access. InNmeaLineToAisStreamAdapterwe access it twice per line (lines 94‑105), meaning duplicate parsing and checksum validation.GetSingleDigitField(lines 249‑273) rejects multi-digit fragment counts. The AIS spec allows up to 9, but third-party data sets often emit 10+, causing avoidable exceptions and re-parses.remainingFields.IndexOf((byte)',')), rescanning the same span. UsingSearchValues<byte>orUtf8Parserover aSequenceReader<byte>can reduce branch mispredictions and take advantage of AVX2.Recommendations:
NmeaTagBlockParserinsideNmeaLineParser(e.g.,private readonly NmeaTagBlockParser? tagBlock;) so consumers do not re-parse.Utf8Parser.TryParseonReadOnlySpan<byte>to accept multi-digit counts while remaining allocation-free.SearchValues<byte>for delimiter scans so the JIT can vectorize them automatically.Impact: Eliminates redundant work during fragment-heavy loads and removes latent incompatibilities with newer AIS feeds.
4. Fragment reassembly (
NmeaLineToAisStreamAdapter.cs)Issues:
NmeaLineParserinstances multiple times—once when first received, again while summing payload lengths, and again during reassembly.fragmentGroupIdsToRemoveusesstackalloc int[this.messageFragments.Count]. Under high traffic, the dictionary can reach thousands of entries and blow the stack (especially on macOS where the guard page is small).TryGetValue/Add). .NET 8 introducedCollectionsMarshal.GetValueRefOrAddDefault, which can mutate entries in-place without allocations.parsedLine.TagBlockis read twice (lines 92‑101), recreating the parser each time (see §3).Recommendations:
parsedLine.Payload.Length, copy just payload bytes, and track padding per fragment. When all fragments arrive, stitch payload spans viaIBufferWriter<byte>without re-parsing.CollectionsMarshal.GetValueRefOrAddDefault(net8+) to fetch or createFragmentedMessagewithout double hashing. For netstandard builds, leave the current path via#if.stackallocremoval list withArrayPool<int>.Shared.Rent(Math.Min(messageFragments.Count, 1024))or a reusableValueListBuilder<int>.NmeaTagBlockParserup-stack so we do not rehydrate it per property access.Impact: Cuts fragment reassembly CPU cost roughly in half, mitigates high-memory workloads, and prevents stack overflows during fragment storms.
5. Bit-vector & payload decoding (
NmeaAisBitVectorParser.cs,NmeaPayloadParser.cs,AisStrings.cs)NmeaPayloadParser.AisAsciiTo6Bits(lines 37‑45) performs nested ternary checks and allocates exception strings on every invalid byte. Modern runtimes provideArgumentOutOfRangeException.ThrowIfNegativeandThrowHelperto keep the happy path branchless.GetUnsignedInteger(lines 67‑138) processes at most 6 bits per loop iteration and callsAisAsciiTo6Bitsfor every chunk. For a 256-bit payload, this means ~43 calls even if later fields re-read the same 6-bit value.GetSignedInteger(lines 43‑59) recomputes sign masks; we can sign-extend via bit shifts (int shift = 32 - (int)bitCount; return ((int)value << shift) >> shift;).ReadOnlySpan<byte> Lookup = ...) or evenVector128<byte>comparisons can translate 16 bytes at a time.Recommendations:
Span<uint>(6-bit values) stored either on the stack (for short payloads) or in a pooled buffer. KeepBitCountand re-use the decoded span in allGet*calls.BitReadersemantics (e.g.,TryReadUInt32(bitCount, out uint value)returning aref struct), enabling consumers to sequentially advance without recomputing offsets.ArgumentOutOfRangeException.ThrowIfGreaterThan) for invalid ASCII to keep the hot path branch-free.static ReadOnlySpan<byte> SixBitLookup => "................................0123456789:;<=>?@ABCDEFGHIJKLMNO...................PQRSTUVWXYZ[\\]^_abcdefghijklmno...................pqrstuvwxyz{|}~"u8;` to map ASCII directly.Impact: Reduces per-field latency, enabling real-time decoding of high-density AIS data while preserving zero-allocation guarantees.
6. Downstream processors (
ReadAllPositions.cs,InspectMessageType.cs)NmeaPayloadParser.PeekMessageType, which re-validates the payload and decodes the first 6 bits for every consumer. Consider caching the type when the message is first parsed and passing it alongside the payload.ReadAllPositionsconstructs new parser structs for each message type. With multi-million message streams, that’s a lot of redundant work when consumers only need a subset of fields. Introducing specialized fast-paths (e.g., “just read lat/long + speed”) would reduce instruction count.Recommendations: Add a lightweight metadata struct (message type, padding, predecoded
NmeaAisBitVectorParser) that can be passed to processors, and expose APIs for reading subsets of fields without rebuilding parser state.7. Benchmarks & diagnostics (
Ais.Net.Benchmarks)GlobalSetuprewrites a 1 M line file synchronously (File.ReadAllLines+StreamWriter) every run, introducing noise and blocking the thread pool during CI. Persist the generated data as part of the repo or use memory-mapped files so that the dataset is materialized once.HardwareCounters(instructions, cache misses) andEventPipeProfilerto capture GC pressure.NmeaStreamParser.ParseStreamAsynctasks) and native AOT builds so regressions show up before shipping.Impact: More reliable perf baselines and richer telemetry whenever parser changes are introduced.
8. Observability & correctness
NmeaLineToAisStreamAdapter.FreeRentedBufferswrites toConsole, which is problematic in services. Replace with structured logging/event counters so operators can monitor fragment drops without stdout parsing.System.Diagnostics.Metricscounters (lines processed, fragments dropped, average message latency) to feed OpenTelemetry. Modern .NET makes this zero-cost when no listener is attached.By implementing the above, the Solutions codebase can fully leverage the capabilities of .NET 10/C# 14, minimize allocations, and keep throughput competitive on modern CPU architectures while retaining compatibility with older deployments.
Opus 4.5 inside Claude Code
Ais.Net Performance Improvements for .NET 10 / C# 14
Executive Summary
This document outlines performance improvements for the Ais.Net library, targeting the transition from .NET Standard 2.0/2.1 to .NET 10 and C# 14. The library is already well-optimized with zero-allocation parsing using
ref structandReadOnlySpan<byte>. The improvements below leverage 6+ years of .NET runtime advancements, modern CPU intrinsics, and new language features to further enhance throughput for mission-critical AIS message processing.Priority Classification:
1. Target Framework Modernization
Current State
Recommendation (P0)
Add modern TFMs while maintaining backwards compatibility:
Rationale:
#if NET8_0_OR_GREATERdirectives2. SIMD/Vectorization Opportunities
2.1 AIS ASCII to 6-Bit Conversion (P0)
File:
NmeaPayloadParser.cs:37-45Current Implementation:
Proposed Improvement:
Expected Impact: 2-4x throughput for payload decoding on AVX2-capable hardware.
2.2 Bit Vector Extraction (P1)
File:
NmeaAisBitVectorParser.cs:67-138Current Implementation:
The
GetUnsignedIntegermethod processes bits character-by-character in a loop.Proposed Improvements:
BitOperationsclass (.NET 5+)Expected Impact: 20-40% reduction in per-field extraction time.
2.3 SearchValues for Delimiter Scanning (P0)
File:
NmeaLineParser.cs,NmeaTagBlockParser.cs,NmeaStreamParser.csCurrent Implementation:
Proposed Improvement (.NET 8+):
Expected Impact: 15-30% faster delimiter scanning, especially for long lines.
3. Memory Allocation Elimination
3.1 UTF-8 String Literals (P0)
File:
NmeaLineParser.cs:17-18Current Implementation:
Proposed Improvement (C# 11+):
Rationale:
ReadOnlySpan<byte>returned from ROM3.2 Stream Parser Buffer Pooling (P1)
File:
NmeaStreamParser.cs:116Current Implementation:
Proposed Improvement:
Note: For very hot paths, consider
stackallocwith a reasonable limit:3.3 FrozenDictionary for Fragment Tracking (P2)
File:
NmeaLineToAisStreamAdapter.cs:21Current Implementation:
Proposed Improvement:
While fragments are dynamic, consider:
3.4 ThrowHelper Pattern for Exception Hot Paths (P0)
Files: All parser files
Current Implementation:
Proposed Improvement:
Rationale:
[DoesNotReturn]helps JIT understand control flow4. Modern C# Language Features
4.1 Primary Constructors (C# 12+) (P3)
File:
NmeaTagBlockSentenceGrouping.csCurrent:
Proposed:
4.2 Collection Expressions (C# 12+) (P3)
File:
NmeaLineParser.csWhere applicable, replace array initializations:
4.3 Pattern Matching Improvements (P2)
File:
NmeaLineParser.cs:90-118Current (switch expression is good, but can be enhanced):
Alternative using tuple patterns:
4.4 Required Members (C# 11+) (P3)
File:
NmeaParserOptions.cs5. Async and I/O Improvements
5.1 High-Precision Timing (P1)
File:
NmeaStreamParser.cs:109-110Current:
Proposed:
Rationale:
Stopwatch.GetTimestamp()provides nanosecond precision on modern systems.5.2 IAsyncEnumerable Support (P2)
New API Addition:
5.3 RandomAccess for File I/O (P2)
File:
NmeaStreamParser.cs:79Current:
Alternative for certain scenarios:
5.4 ConfigureAwait Improvements (P3)
.NET 8+ consideration:
6. Bit Manipulation Optimizations
6.1 Branchless Minimum (P1)
File:
NmeaAisBitVectorParser.cs:89Current:
Proposed:
Or use
Math.Minand trust the JIT (modern .NET JIT often emits branchless code for simpleMath.Minpatterns).6.2 Sign Extension Optimization (P1)
File:
NmeaAisBitVectorParser.cs:43-58Current:
Proposed (branchless sign extension):
Expected Impact: Eliminates branch in sign extension, ~10-15% faster for signed integer fields (latitude, longitude, rate of turn).
7. Span and Memory Optimizations
7.1 SequenceReader for Buffer Parsing (P2)
File:
NmeaStreamParser.csProcessBuffer methodConsideration:
Note: Current implementation is already efficient;
SequenceReadermay add overhead for simple newline scanning.7.2 Span Slicing in Loops (P2)
File:
NmeaAisTextFieldParser.cs:67-73Current:
Proposed (batch processing):
Further optimization: inline
GetAsciilogic to avoid per-character method call overhead.8. JIT and Codegen Hints
8.1 Aggressive Inlining Attributes (P1)
Add
[MethodImpl(MethodImplOptions.AggressiveInlining)]to:AisAsciiTo6BitsGetUnsignedInteger(for small bit counts)GetBitAisCharacterToAsciiValue8.2 SkipLocalsInit (P2)
For hot allocation paths:
Caution: Only use where you're certain all locals are written before read.
8.3 Suppress GC Transitions (P3)
For extremely hot paths with no managed allocations:
9. API Additions for Modern Consumers
9.1 Generic Math Support (P3)
File: New
IAisPositionReportinterface9.2 Utf8JsonWriter Support (P2)
10. Benchmarking Recommendations
10.1 Update Benchmark Project
10.2 Key Benchmarks to Add
10.3 BenchmarkDotNet Configuration
11. Implementation Priority Matrix
12. Migration Strategy
Phase 1: Foundation
#ifconditional compilation structurePhase 2: Core Optimizations
Phase 3: Advanced Optimizations
Phase 4: API Enhancements
13. Risks and Mitigations
#if14. Conclusion
The Ais.Net library has an excellent foundation with its zero-allocation
ref structdesign. The improvements outlined here can provide:These improvements maintain full backwards compatibility through conditional compilation while giving users on modern .NET runtimes significant performance gains for their mission-critical AIS processing workloads.
Document Version: 1.0
Analysis Date: December 2024
Target: .NET 10 / C# 14