Skip to content

De-reflect + DRY the array/enumerable converters (P4) - #25

Merged
guy-lud merged 4 commits into
masterfrom
perf/p4-dereflect-converters
Jul 13, 2026
Merged

De-reflect + DRY the array/enumerable converters (P4)#25
guy-lud merged 4 commits into
masterfrom
perf/p4-dereflect-converters

Conversation

@guy-lud

@guy-lud guy-lud commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

P4 — de-reflect + DRY the array/enumerable converters

De-reflects the collection conversion hot path and removes the near-duplication between the two collection converters.

What changed

  • New Conversion/CollectionTypeConverter — a shared base implementing Convert once: normalize the incoming value to an array (split a delimited string / pass an existing array through / wrap a scalar), select the element converter by walking the concrete LinkedList (struct enumerator — no boxed enumerator, no predicate closure), then fill an Array.CreateInstance(elementType, n) by index.
  • ArrayTypeConverter / EnumerableTypeConverter collapse to thin subclasses that differ only in CanConvert + element-type extraction. Both now return T[] — safe because IsEnumerable() matches only IEnumerable<T>, which a T[] satisfies.
  • Removed from the per-convert path: the List<T> + its backing array, the reflected Enumerable.ToArray (MakeGenericMethod + Invoke + args array), and the First predicate closure.
  • TypeConverter.CreateNullResult swaps the Enumerable.Empty<T>() reflection for Array.CreateInstance(t, 0); GetConverter is now a true manual walk (matching its own comment). Also strips a stray UTF-8 BOM from the file.

Proof

New gated ConvertArrayBenchmark (isolates the hot path like the Q1/Q3/Q4 micro-benchmarks); ShortRun on net10.0:

Allocated Mean
Before 1.33 KB 1,247 ns
After 688 B 219 ns
Δ −49% 5.7× faster

The residual 688 B is the split-substrings + per-element boxing + result array, shared by both the old and new code (irreducible without changing the object Convert(object, Type) element contract).

Tests

+7 collection-converter parity tests (Conversion/CollectionConversionTests.cs): delimited string → int[] / string[] / IEnumerable<int>, empty-entry removal, custom delimiter, default-array passthrough, and that the enumerable path now materializes a T[]. Suite: 63 per TFM (was 56).

Also in this PR

Refreshes SESSION-HANDOFF.md + FIX-PLAN.md, and carries the pre-P4 post-P3 style tweaks to TypeConverter.cs / TypeExtensions.cs that were already in the working tree meant to ride this branch.

guy-lud added 4 commits July 13, 2026 11:36
Introduce a shared CollectionTypeConverter base that builds collection
results with Array.CreateInstance + indexed fill and selects the element
converter by walking the concrete LinkedList (struct enumerator) rather
than LINQ First. ArrayTypeConverter/EnumerableTypeConverter collapse to
thin subclasses that differ only in CanConvert + element-type extraction;
both now return T[] (safe: IsEnumerable() matches only IEnumerable<T>,
which a T[] satisfies). This drops the per-convert List<T> + its backing
array, the reflected Enumerable.ToArray (MakeGenericMethod + Invoke +
args array), and the First predicate closure.

TypeConverter.CreateNullResult swaps the Enumerable.Empty<T>() reflection
for Array.CreateInstance(t, 0), and GetConverter is now a true manual walk
(matching its own comment). Also strips a stray UTF-8 BOM from the file.

Proof via the new gated ConvertArrayBenchmark (isolates the hot path like
Q1/Q3/Q4): 1.33 KB -> 688 B (-49%), 1,247 -> 219 ns (5.7x). The residual
688 B is the split-substrings + per-element boxing + result array shared
by both the old and new code.

Adds 7 collection-converter parity tests (delimited string -> int[] /
string[] / IEnumerable<int>, empty-entry removal, custom delimiter,
default-array passthrough, and that the enumerable path materializes a
T[]). Suite: 63 tests per TFM (was 56).

Also refreshes SESSION-HANDOFF.md + FIX-PLAN.md and carries the pre-P4
post-P3 style tweaks to TypeConverter.cs / TypeExtensions.cs.
Adds 5 collection-converter tests the dotnet code-reviewer suggested:
- CreateNullResult null path: an unbound IEnumerable<T> with no default now
  yields an empty T[] -- the one line P4 changed in TypeConverter.cs that no
  existing test exercised (all others bind a value or supply a default).
- Element-converter parity: DayOfWeek[], DateTime[], Uri[] (the shipped tests
  only covered int/string, both routed to DefaultTypeConverter).
- Negative: a non-numeric element for an int[] surfaces as the expected
  SettingsPropertyValueException, pinning the exception-wrapping contract.

Suite: 68 per TFM (was 63). No production changes. Refreshes
SESSION-HANDOFF.md + FIX-PLAN.md (counts, PR #25, code-review outcome).
@guy-lud
guy-lud merged commit f9a3061 into master Jul 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant