Repository navigation
Read ISO 8211 subfields without allocating per value - #32
Merged
Merged
Conversation
Iso8211FieldReader allocated about 430 bytes per coordinate: a LINQ closure, delegate and enumerator for every name lookup, a boxed value for every read, a group object per repetition, and a subfield offset table built in a List and copied by ToArray. - Work out each field definition's layout once (Iso8211FieldLayout, cached per definition in a ConditionalWeakTable, not on the record): subfield names, formats and, when every subfield is fixed width, the byte offsets of a repetition. Fixed-width fields such as SG2D, SG3D, FSPT and VRPT then need no per-instance offset table. - Convert values to T without boxing, and decode character data once. - Add GetOrdinal plus GetSubfield<T>/TryGetSubfield<T>/GetSubfieldBytes by group index and ordinal, for reading long repeating groups without a name lookup or group object per repetition. Additive only. - Replace LINQ lookups in GetSubfieldDefinition, GetFieldDefinition, GetFieldByTag and GetFieldsByTag with loops. - Use the ordinal accessors in S57DocumentReader's group loops, keeping its warn-and-skip handling of malformed fields. S57DocumentReader on the synthetic harbour cell allocates 8.0 MB, down from 29.0 MB. Behavior is pinned by a frozen copy of the previous field reader that a differential test compares against (synthetic, truncated, hand-built and, locally, real cells) and by fingerprints of the S-57 documents the reader produced before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
The fingerprint tests failed on Windows: StringBuilder.AppendLine writes Environment.NewLine, so the hashed text had CRLF line endings there. End lines with '\n' and format numbers with the invariant culture, which also keeps a culture with a different minus sign (sv-SE uses U+2212) from changing the hash. The hashes are unchanged on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Items 2 and 3 of the performance plan: precompute each field definition's layout once, and decode subfield values without allocating. No existing public signature changes; the new API is additive.
Why
Iso8211FieldReaderallocated about 430 bytes per coordinate:ParsedSubfieldtable built in aList, then copied byToArrayIso8211SubfieldGroupper repetitionGetSubfield("YCOO"): LINQFirstOrDefaultclosure, delegate and enumerator, twiceConvertValue<T>boxing each value asobjectS100's
S101DocumentReaderuses the same pattern for S-101 data (133 uses), so it gains from these changes once it updates the package.Changes
Iso8211FieldLayout(internal) works out a definition's subfield names, formats and repeating-group shape once.ConditionalWeakTable, not stored on the record. A cached member on theIso8211FieldDefinitionrecord would take part in its generated equality, and awithexpression would copy a stale layout.Conversions return
Tdirectly. Each(T)(object)valuesits behind atypeof(T) == typeof(…)check, which optimized code compiles to no box. UTF-8 character data is trimmed before decoding, so it decodes once and no longer allocatesTrimEnd's parameter array.New, additive API, in the style of ADO.NET's
GetOrdinal/GetInt32(i):GetOrdinal(name)GetSubfield<T>(groupIndex, ordinal)andTryGetSubfield<T>(groupIndex, ordinal, out value)GetSubfieldBytes(groupIndex, ordinal)The name-based members are unchanged and are now non-allocating too.
LINQ replaced with loops in
GetSubfieldDefinition,GetFieldDefinition,GetFieldByTagandGetFieldsByTag.S57DocumentReaderuses the ordinal accessors in its six group loops. It keeps its "log a warning and stop" handling for malformed fields, including a field cut off partway through a repetition.Docs:
docs/reading-iso8211.mdgains a short example of reading a long repeating group by ordinal. The example was compiled and run against a real cell.Results
Memory budgets (synthetic harbour cell; exact figures):
S57DocumentReader.ReadGetSubfieldGroups+ namesOn the older 3.5 MB US4MD81M edition,
S57DocumentReaderwent from 203 MB to 62 MB allocated, and roughly from 290 ms to 185 ms. The timings come from a shared, noisy machine, so treat them as indicative; this PR's Benchmarks job gives the base-vs-head comparison. Most of what remains is the intermediateIso8211Documentand its GC cost, which the next PR targets.How behaviour is pinned
Iso8211FieldReaderEquivalenceTestscompares the new reader withLegacyIso8211FieldReader, a frozen copy of the previous implementation kept undertests/…/Reference/, on:VCID*YCOO!XCOO!ZCOO), and lexical level 2.It also checks the new ordinal accessors against the group objects. Locally, it also passed on all 19 real cell and update files with
ENCDOTNET_ENC_ROOTset.S57DocumentReaderFingerprintTestspins SHA-256 fingerprints of the S-57 documents the reader produced before this change, for the three synthetic profiles and two update chains. Locally, fingerprints of the real cell and update files are also identical before and after.One subtle difference
Name lookups now match subfields by position in
SubfieldDefinitions. Previously they matched on the definition'sIndexproperty. The DDR reader always setsIndexequal to the position, so parsed data behaves identically. Only a hand-built definition whoseIndexvalues disagree with their positions would read differently, and it previously returned the wrong subfield.Standards alignment
iso8211)s57-enc)chart-viewer)Standard section references cited in code or docs: none new; subfield parsing rules are unchanged.
Tests
tests/dotnet test --configuration Releasepasses locally: 517 unit tests and 13 performance tests. Unit test time rose from about 1 s to 9 s, mostly from the comparison and fingerprint tests.dotnet format(CI diagnostics) anddotnet packwith package validation against 0.7.0 both pass.Documentation
src/<project>/README.mddocs/if user-facing behavior changeddocfx docfx.json --warningsAsErrorspasses, if docs changed. docfx isn't installed in the dev sandbox; the docs workflow will check it.Dependencies
Directory.Packages.props(not in the.csproj)gh-advisory-databasesecurity check run for any new dependencyBreaking changes
None. Package validation against 0.7.0 passes: the new members are additions. EncDotNet.S100 needs no changes, and its S-101 reader benefits on its next package update.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
Generated by Claude Code