# AcBinary Wire — Hot/Cold Bytecode Layout

Working notes for a wire-format reorganization where the BinaryTypeCode marker space is partitioned into a **hot range** (no-feature common path) and a **cold range** (feature-engaged paths). Single-branch dispatch on the marker byte (`(tc & 0x80) == 0` or `tc < ColdStart`) selects between an inline hot switch and a per-category cold method.

> Not a TODO entry. LLM-only working notes. Sibling to `BINARY_SGEN_OPTIMIZATION.md` (per-property emit condensation companion).

> ⚠️ **Needs revision** — several sections (esp. "Boundary aligns with feature flags", "Strict no-feature collapse") assume per-type `EnableRefHandlingFeature = false` removes `ObjectRef*` markers from the wire. That assumption is **false**: the runtime writer emits `ObjectRef*` based on child-type metadata, NOT the parent's per-type flag. The parent-flag governs only self-tracking emit. See `RefAwareEmitPredicate` in `AcBinarySourceGenerator.Models.cs` and `AcBinarySerializableAttribute.EnableRefHandlingFeature` doc for the correct scope. Re-evaluate the hot/cold boundary candidates accordingly: `EnableInternString` / `EnableMetadata` stripping is still wire-symmetric and valid; `EnableRefHandling` stripping is NOT.

## Core concept

The marker-byte tier reflects the wire-format's perf-architecture: hot markers serve the no-feature workload at maximum speed (small inline switch in SGen-emit), cold markers carry the feature-engaged variants behind a single method call.

**Boundary aligns with feature flags:**

- ~~`EnableRefHandling=false` → `ObjectRef*`, ref-aware element/dict wrappers never on wire~~ — **not true**: the parent-flag controls only self-tracking emit; `ObjectRef*` markers may still appear on the wire from child-type ref-handling decisions. Excluded from the hot/cold boundary candidates.
- `EnableInternString=false` → `StringInterned`, `StringInternFirst*` never on wire
- `EnableMetadata=false` → `ObjectWithMetadata*` never on wire
- `EnablePolymorphDetect=false` → `ObjectWithTypeName*`, FixObj slot fallback never on wire

A type with the symmetric features off (Intern / Metadata / Polymorph) emits **only hot markers** in those categories; the reader's cold branch for those is provably dead code for that type — JIT-foldable when SGen-emit can prove it at compile time. `RefHandling` does NOT participate in this collapse.

### Strict no-feature collapse — zero regression baseline

When SGen proves the **symmetric** feature flags off for a type (`EnableInternString = EnableMetadata = EnablePolymorphDetect = false`) AND the type has no children with active ref-tracking (`ChildNeedsRefScan` / `ElementNeedsRefScan` / `DictValueNeedsRefScan` all false), the cold branch AND the `IsHot` check itself **both** drop from the generated emit:

```csharp
// Strict no-feature emit — no IsHot check, no cold fallback
switch (tc) {
    case BinaryTypeCode.Object: ... ; break;
    case BinaryTypeCode.Null:   ... ; break;
}
```

The strict no-feature SGen-emit is **structurally identical to the original pre-refactor inline emit** — every dispatch the same shape, same instructions. **Zero regression** on the no-feature hot path. The hot/cold infrastructure adds nothing the type doesn't need.

Feature-engaged types pay the IsHot check (3-4 cmp) plus the cold method call (when a cold marker hits) — both costs covered many times over by the feature's own value (see cost-benefit below).

## Hot vs cold markers across all categories

| Category | HOT markers (no-feature) | COLD markers (feature-engaged) |
|---|---|---|
| String | `FixStrAscii (range)`, `StringSmall`, `Null`, `StringEmpty` | `StringMedium`, `StringBig`, `StringAscii (long)`, `StringInterned`, `StringInternFirstSmall/Medium` |
| Complex | `Object`, `Null` | `ObjectRefFirst`, `ObjectRef`, `ObjectWithMetadata`, `ObjectWithMetadataRefFirst`, `ObjectWithTypeName*`, `FixObj slot (0-63)` |
| Collection element | `Array`, `EmptyArray`, `Null` | Ref-aware element wrappers, ChunkedArray variants |
| Dictionary entry | `Dict`, `EmptyDict`, `Null` | Ref-aware key/value variants |
| Primitive | `Int32`, `Int64`, `Double`, `Decimal`, `Enum`, `TinyInt (range)`, `Boolean`, `Guid`, etc. | Large-variant primitive markers (rare) |

## Two implementation paths

### B — `IsHotXxx` helper, wire-format intact (preferred for incremental)

Per-category small helpers on `BinaryTypeCode`:

```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsHotStringMarker(byte tc) =>
    IsFixStrAscii(tc) || tc == StringSmall || tc == Null || tc == StringEmpty;
```

SGen-emit:
```csharp
if (BinaryTypeCode.IsHotStringMarker(tc)) {
    // inline hot switch (3-5 case)
} else {
    obj.X = context.ReadStringCold(tc);
}
```

- 3-4 cmp instructions inlined per dispatch site (branch predictor learns common-case bias)
- Zero wire-format change; persisted blobs unaffected
- Per-category helper introduced incrementally, switch-by-switch validation
- No global atomic refactor required

### A — Renumber BinaryTypeCode, hot range first

Hot codes contiguous (e.g. 0x00-0x7F), cold (0x80-0xFF). Single dispatch:

```csharp
if ((tc & 0x80) == 0) {
    // hot inline switch
} else {
    obj.X = context.ReadStringCold(tc);
}
```

- 1 `test+jne` instruction per dispatch site (minimum overhead)
- Hot switch contiguous range → JIT may emit jump table
- Wire-format breaking change — version bump, persisted blob migration required
- Architecturally cleanest but disruptive

**Recommended sequence:** start with B (incremental, reversible), graduate to A when a wire-format-breaking change is incurred for another reason anyway.

## Per-category cold dispatch methods

Each on the `BinaryDeserializationContext<TInput>`, `[MethodImpl(MethodImplOptions.AggressiveOptimization)]` (Tier-1 direct, skip QuickJIT):

| Method | Category | Markers handled |
|---|---|---|
| `ReadStringCold(byte tc)` | String | Medium, Big, Ascii-long, Interned, InternFirstSmall, InternFirstMedium |
| `ReadComplexCold<T>(byte tc)` | Complex | ObjectRefFirst, ObjectRef, ObjectWithMetadata*, FixObj-slot fallback, ObjectWithTypeName* |
| `ReadCollectionElementCold<T>(byte tc)` | Collection element | Ref-aware element wrappers, large-array variants |
| `ReadDictEntryCold<TK, TV>(byte tc)` | Dictionary entry | Ref-aware key/value wrappers |
| `ReadEnumCold(byte tc)` | Enum | Rare numeric variants |
| `ReadPrimitiveCold[T](byte tc)` | Primitive | Large-variant primitives (rare) |

Generic specialization on the complex/collection/dict cold methods multiplies native bodies per `<T>` — acceptable cost given Tier-1 optimization and rare hot-path engagement. Alternative: erase to `Type` parameter on cold methods to keep one native body — Tier-1 amortizes reflection cost.

## Single source of truth — category-bounded

The hot/cold method-set ideal — "SGen-emit and runtime path call identical methods" — applies **fully** for categories without type-specific dispatch, **partially** for those with it:

| Category | Marker dispatch shared? | Type-specific work shared? | Notes |
|---|:---:|:---:|---|
| String | ✅ | ✅ (no type-specific) | One `ReadStringHotMarker(tc)` / `ReadStringColdMarker(tc)` pair, both paths call same |
| Primitive (Int32/Int64/Double/Decimal/Guid/...) | ✅ | ✅ (no type-specific) | Same |
| Enum | ✅ | ✅ (cast-only, generic) | Same |
| Complex | ✅ | ⚠️ Diverges | SGen: generic-static-abstract direct dispatch. Runtime: TypeReaderTable + `GetWrapper(Type)` metadata-driven (current architecture preserved) |
| Collection / Dictionary | ✅ | ⚠️ Diverges | Same as Complex — element/key/value reader is type-specific |

### Type-specific dispatch divergence

For Complex (and Collection-element / Dict-entry), the type-specific reader call diverges between paths because C# generic methods cannot accept a runtime `Type` as a generic parameter without reflection (`MethodInfo.MakeGenericMethod(...).Invoke(...)`) — not viable on the hot path (slow, AOT-trim-hostile).

**SGen path** — both type and reader known at compile time, uses generic + static abstract:

```csharp
// Existing precedent: IBinaryInputBase.IsTrustedSingleSegment (static abstract bool) — production-used,
// NativeAOT-verified via the AyCode.Core.Serializers.Console.csproj <PublishAot>true</PublishAot> path.
internal interface IGeneratedBinaryReader<TSelf>
    where TSelf : IGeneratedBinaryReader<TSelf>
{
    static abstract void ReadProperties<TInput>(object value, BinaryDeserializationContext<TInput> context)
        where TInput : struct, IBinaryInputBase;
}

// Context method:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal T ReadHotComplexMarker<T, TReader>(byte tc)
    where T : class, new()
    where TReader : IGeneratedBinaryReader<TReader>
{
    var rc = new T();
    TReader.ReadProperties(rc, this);  // static abstract — JIT direct call per-spec
    return rc;
}

// SGen-emit:
obj.Tag = ctx.ReadHotComplexMarker<SharedTag_All_True, SharedTag_All_True_GeneratedReader>(tc);
```

ILC sees concrete `<T, TReader>` pair at the SGen-emit call site → generates specialization → direct call in native code. No reflection, no DAM-attribute required on the hot path.

**Runtime path** — type known only at runtime, keeps current architecture:

- `TypeReaderTable<TInput>.Readers[tc]` for marker dispatch (could be migrated to direct switch-call pattern but a separate refactor)
- `GetWrapper(Type)` for type-specific Complex dispatch (metadata + compiled-expression property accessors)
- The existing `IGeneratedBinaryReader` (non-static-abstract) interface stays — used by runtime cross-type populate path

### Net wins per path

- **SGen path:** full hot/cold method-set with generic-static-abstract for Complex/Collection/Dict. Maximum perf, single dispatch model, JIT-inline-friendly.
- **Runtime path:** marker dispatch shared with SGen (String / Primitive / Enum hot+cold methods). Type-specific dispatch unchanged (TypeReaderTable + metadata). Gains the marker-dispatch hot path, not the type-specific.

## Cost-benefit — why this is win-win, not trade-off

Feature-cold path cost vs feature-engaged value:

| Feature | Cold-path overhead (per use) | Feature benefit (per use) | Net |
|---|---:|---:|---:|
| String interning | ~5 ns (cold method call) | ~50 B/repeat (50-char strings) + ~50 ns UTF-8 encode skipped + intern-cache O(1) dedup | **10× positive** |
| Id-tracking (IId ref) | ~5 ns | Whole subgraph re-serialization skipped: ~100-1000 ns + N×wire-bytes saved | **20-200× positive** |
| Ref-handling (hash-based) | ~5 ns | Non-IId reference dedup: ~50-200 ns + N×wire-bytes saved | **10-40× positive** |
| Metadata mode | ~5 ns | Schema-evolution support, mismatched-property tolerance | Correctness, not perf |
| Polymorphism | ~5 ns | Runtime type dispatch — no alternative on the wire | Correctness, not perf |

**Conclusion:** the cold-path method-call cost is paid back many times over by the feature's own value. No-feature workloads pay zero. Feature-engaged workloads pay ~5 ns to save 50-1000 ns. Both ends benefit.

## Implementation sequence

**The whole approach is gated on a JIT-inlining empirical check.** The TryReadStringProperty (Ötlet A) attempt failed because the inliner bailed on the body — the proposed hot/cold split aims to fix this by shrinking the hot body, but **JIT inliner decisions are opaque** and can only be confirmed empirically. The sequence below is **falsifiable** — Phase 0 is a kill-switch.

### Phase 0 — Disasm prototype (kill-switch gate)

Test the **worst realistic candidate**, not the easiest. If String — the largest hot body in the proposed architecture — inlines successfully, every other category (Primitive, Enum, Complex, Collection, Dict) is smaller and will inline too. If String fails, the architecture is not viable on the current JIT regardless of how the smaller categories would fare. (Testing Enum first proves nothing — its ~10-15 IL body is far below any plausible inline threshold.)

Build the hot string-marker method (3 marker cases + FixStrAscii range check):

```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal string? ReadHotStringMarker(byte tc)  // caller already read byte + checked PropertySkip
{
    if (BinaryTypeCode.IsFixStrAscii(tc))
    {
        var len = BinaryTypeCode.DecodeFixStrAsciiLength(tc);
        return len == 0 ? string.Empty : ReadAsciiBytesAsString(len);
    }
    return tc switch {
        BinaryTypeCode.StringSmall  => ReadStringSmall(),
        BinaryTypeCode.Null         => null,
        BinaryTypeCode.StringEmpty  => string.Empty,
        _ => null  // unreachable — caller guaranteed hot range
    };
}
```

**Source-level if-chain vs switch is stylistic for this case count.** Roslyn converts small (≤3-4) and sparse-value switches to cmp-chain IL (`bne`/`beq` sequence), not the `switch` IL instruction. Our marker values (91/92/93 non-contiguous) force cmp-chain regardless of source form. The inliner's complexity-scoring sees identical IL either way — no inline-decision difference between forms.

Add a single SGen-emit call site, build, run:
```
DOTNET_JitDisasm=*ReadProperties* DOTNET_TC_QuickJit=0 dotnet run ...
```

Inspect the caller body in the emitted disasm:
- **Inline success:** `ReadHotStringMarker` body fully inlined — `IsFixStrAscii` check + marker dispatch + the inner `ReadStringSmall` / `ReadAsciiBytesAsString` calls appear directly in `ReadProperties`. → architecture VIABLE for String → other categories almost certainly inline too → proceed to Phase 1.
- **Inline failure:** caller body contains `call ReadHotStringMarker` — JIT bailed. → architecture NOT VIABLE on this JIT for the String hot body. Document outcome as Tanulság (lesson-learned). Keep current inline-emit for String; consider proceeding for smaller-body categories (Primitive/Enum) where success is more likely.

**Cost:** ~2-3 hours. Decision is per-category (String fail doesn't automatically kill Primitive/Enum).

### Phase 1 — Build out viable categories (only if Phase 0 ✓)

1. **Audit BinaryTypeCode** — current values per category, hot vs cold classification
2. **B path: per-category `IsHot*` helpers** — `BinaryTypeCode.IsHotStringMarker`, `IsHotComplexMarker`, etc.
3. **Per-category hot+cold method pairs** — `BinaryDeserializationContext.ReadHotXxxMarker(tc)` (`[AggressiveInlining]`) + `ReadColdXxxMarker(tc)` (`[AggressiveOptimization]`)
4. **SGen-emit conversion, one category at a time** with per-category disasm-recheck:
   - **Primitive** first (smallest blast radius, simplest body)
   - Disasm-verify hot method inlines at caller site → BDN/Console F median-of-3 → confirm no regression
   - **String** next (slightly larger body — riskier; if Primitive succeeded but String fails, fall back to inline emit just for String)
   - **Enum** (similar to Primitive)
   - **Complex** — also requires `IGeneratedBinaryReader<TSelf>` generic-static-abstract interface refactor; bigger touch surface but follows same inline-verification protocol
   - **Collection / Dictionary** last (element/entry dispatch reuses Complex pattern)
5. **Strict-mode SGen-emit collapse** — when feature flags prove cold dead, emit just the hot switch (no IsHot check, no cold fallback). K9M3 Phase C provides the metadata.
6. **A path (later, gated)** — BinaryTypeCode renumber when a wire-format-breaking change is independently scheduled

### Per-category abort gates

Each category's Phase 1 step has its own disasm-verify checkpoint. If a category fails its inline gate (body too complex despite shrinking attempts), that category falls back to the inline-emit architecture — the others continue. No all-or-nothing commitment.

## Open questions

- Per-category cold method generic specialization vs `Type`-parameter erasure: bin-size vs Tier-1 reflection cost trade-off — measure before committing
- Hot-range size budget: how many markers fit in the proposed hot range (e.g., 0x00-0x7F = 128 codes), accounting for FixStrAscii's 32-byte range consumption + TinyInt range
- ChunkedArray and other large-collection variants: which side of the boundary? (Hot if independent of the symmetric stripping flags above, cold if feature-engaged)
- Cost-benefit on the writer side: does the writer-side cold path (intern-cache hash + lookup, IdentityMap probe) carry the same +/- profile as the reader side?

## Cross-references

- `BINARY_SGEN_OPTIMIZATION.md` — per-property emit condensation companion (sister optimization, same K9M3 ecosystem)
- `BINARY_TODO.md#accore-bin-t-k9m3` — Phase C feature-conditional emit (the layer this builds on)
- `BINARY_FORMAT.md` — current wire-format spec (will need update post-renumber if A path taken)
- `BINARY_SGEN.md` — SGen architecture (the SGen-emit refactor target)
