Refactor charset profiles; split StringSmall decode paths
- Benchmark charset profiles are now length-consistent: all *Short = 40 chars, all *Long = 280 chars, across ASCII, Latin1, CJK BMP, Cyrillic, and Mixed. - `CharsetSuffixes` was rewritten with new profiles and base-string repetition for compile-time constants. - Menu/configuration updated for new profiles, selection logic, and improved descriptions. - Docs updated to reflect new profiles, lengths, and serialization tier impacts. - `StringSmall` deserialization split into `ReadStringSmallCompact` and `ReadStringSmallFastWire`; all call sites now dispatch by mode, clarifying the hot path. - SGen codegen and runtime dispatch tables updated for the new decode split. - Binary marker docs clarified: only Intern/Metadata/Polymorph features are wire-symmetric for reader case omission; RefHandling is not. - Added `BINARY_STRICT_SGEN.md` planning doc for a SGen-only, attribute-required, AOT-friendly NuGet package.
This commit is contained in:
parent
3671c70aa1
commit
b8d0d85c99
|
|
@ -82,11 +82,16 @@ public static class BdnSummaryAdapter
|
|||
return s switch
|
||||
{
|
||||
CharsetSuffixes.Latin1FixAscii => "Latin1FixAscii",
|
||||
CharsetSuffixes.AsciiShort => "AsciiShort",
|
||||
CharsetSuffixes.AsciiLong => "AsciiLong",
|
||||
CharsetSuffixes.Latin1Short => "Latin1Short",
|
||||
CharsetSuffixes.Latin1Long => "Latin1Long",
|
||||
CharsetSuffixes.CjkBmp => "CjkBmp",
|
||||
CharsetSuffixes.Cyrillic => "Cyrillic",
|
||||
CharsetSuffixes.Mixed => "Mixed",
|
||||
CharsetSuffixes.CjkBmpShort => "CjkBmpShort",
|
||||
CharsetSuffixes.CjkBmpLong => "CjkBmpLong",
|
||||
CharsetSuffixes.CyrillicShort => "CyrillicShort",
|
||||
CharsetSuffixes.CyrillicLong => "CyrillicLong",
|
||||
CharsetSuffixes.MixedShort => "MixedShort",
|
||||
CharsetSuffixes.MixedLong => "MixedLong",
|
||||
_ => "Custom"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,11 +85,16 @@ internal static class Configuration
|
|||
return s switch
|
||||
{
|
||||
CharsetSuffixes.Latin1FixAscii => "Latin1FixAscii",
|
||||
CharsetSuffixes.AsciiShort => "AsciiShort",
|
||||
CharsetSuffixes.AsciiLong => "AsciiLong",
|
||||
CharsetSuffixes.Latin1Short => "Latin1Short",
|
||||
CharsetSuffixes.Latin1Long => "Latin1Long",
|
||||
CharsetSuffixes.CjkBmp => "CjkBmp",
|
||||
CharsetSuffixes.Cyrillic => "Cyrillic",
|
||||
CharsetSuffixes.Mixed => "Mixed",
|
||||
CharsetSuffixes.CjkBmpShort => "CjkBmpShort",
|
||||
CharsetSuffixes.CjkBmpLong => "CjkBmpLong",
|
||||
CharsetSuffixes.CyrillicShort => "CyrillicShort",
|
||||
CharsetSuffixes.CyrillicLong => "CyrillicLong",
|
||||
CharsetSuffixes.MixedShort => "MixedShort",
|
||||
CharsetSuffixes.MixedLong => "MixedLong",
|
||||
_ => "Custom"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,12 +107,19 @@ internal static class Menu
|
|||
System.Console.WriteLine("─────────────────────────────────────────────");
|
||||
System.Console.WriteLine($"Current: {Configuration.GetCurrentCharsetName()}");
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine(" [1] Latin1FixAscii — empty suffix; short FixStr-fast-path stress (Latin1 baseline values stay short)");
|
||||
System.Console.WriteLine(" [2] Latin1Short — \" árvíztűrő tükörfúrógép\" (~24 char Hungarian mixed)");
|
||||
System.Console.WriteLine(" [3] Latin1Long — ~47-char Latin1 mixed (default; exceeds FixStr boundary)");
|
||||
System.Console.WriteLine(" [4] CjkBmp — CJK BMP (long 3-byte runs)");
|
||||
System.Console.WriteLine(" [5] Cyrillic — Russian Cyrillic (long 2-byte runs)");
|
||||
System.Console.WriteLine(" [6] Mixed — Hungarian + CJK + Cyrillic + emoji (full-spectrum + surrogate pairs)");
|
||||
System.Console.WriteLine(" All *Short = 40 char, all *Long = 280 char (= Short × 7) — length-consistent across charsets.");
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine(" [1] Latin1FixAscii — empty suffix; baseline-only short values → FixStrAscii tier");
|
||||
System.Console.WriteLine(" [2] AsciiShort — 40 char pure ASCII (quic × 8) → StringAscii tier");
|
||||
System.Console.WriteLine(" [3] AsciiLong — 280 char pure ASCII → StringAscii tier");
|
||||
System.Console.WriteLine(" [4] Latin1Short — 40 char Hungarian (árví × 8) → StringSmall tier");
|
||||
System.Console.WriteLine(" [5] Latin1Long — 280 char Hungarian (default) → StringMedium tier");
|
||||
System.Console.WriteLine(" [6] CjkBmpShort — 40 char CJK BMP (3-byte runs) → StringSmall tier");
|
||||
System.Console.WriteLine(" [7] CjkBmpLong — 280 char CJK BMP → StringMedium tier");
|
||||
System.Console.WriteLine(" [8] CyrillicShort — 40 char Cyrillic (2-byte runs) → StringSmall tier");
|
||||
System.Console.WriteLine(" [9] CyrillicLong — 280 char Cyrillic → StringMedium tier");
|
||||
System.Console.WriteLine(" [0] MixedShort — 40 char multi-codepage → StringSmall tier");
|
||||
System.Console.WriteLine(" [A] MixedLong — 280 char multi-codepage → StringMedium tier");
|
||||
System.Console.WriteLine(" [B] Back");
|
||||
System.Console.Write("\nSelection: ");
|
||||
|
||||
|
|
@ -126,24 +133,44 @@ internal static class Menu
|
|||
System.Console.WriteLine("✓ Charset set to Latin1FixAscii");
|
||||
return;
|
||||
case '2':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.AsciiShort;
|
||||
System.Console.WriteLine("✓ Charset set to AsciiShort");
|
||||
return;
|
||||
case '3':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.AsciiLong;
|
||||
System.Console.WriteLine("✓ Charset set to AsciiLong");
|
||||
return;
|
||||
case '4':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.Latin1Short;
|
||||
System.Console.WriteLine("✓ Charset set to Latin1Short");
|
||||
return;
|
||||
case '3':
|
||||
case '5':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.Latin1Long;
|
||||
System.Console.WriteLine("✓ Charset set to Latin1Long");
|
||||
return;
|
||||
case '4':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.CjkBmp;
|
||||
System.Console.WriteLine("✓ Charset set to CjkBmp");
|
||||
return;
|
||||
case '5':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.Cyrillic;
|
||||
System.Console.WriteLine("✓ Charset set to Cyrillic");
|
||||
return;
|
||||
case '6':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.Mixed;
|
||||
System.Console.WriteLine("✓ Charset set to Mixed");
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.CjkBmpShort;
|
||||
System.Console.WriteLine("✓ Charset set to CjkBmpShort");
|
||||
return;
|
||||
case '7':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.CjkBmpLong;
|
||||
System.Console.WriteLine("✓ Charset set to CjkBmpLong");
|
||||
return;
|
||||
case '8':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.CyrillicShort;
|
||||
System.Console.WriteLine("✓ Charset set to CyrillicShort");
|
||||
return;
|
||||
case '9':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.CyrillicLong;
|
||||
System.Console.WriteLine("✓ Charset set to CyrillicLong");
|
||||
return;
|
||||
case '0':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.MixedShort;
|
||||
System.Console.WriteLine("✓ Charset set to MixedShort");
|
||||
return;
|
||||
case 'a':
|
||||
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.MixedLong;
|
||||
System.Console.WriteLine("✓ Charset set to MixedLong");
|
||||
return;
|
||||
case 'b':
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -38,16 +38,23 @@ Workload + reporting types — `ISerializerBenchmark`, `BenchmarkResult`, `Bench
|
|||
|
||||
## Charset profiles (Menu → Settings → Charset)
|
||||
|
||||
Controls the `BenchmarkTestDataProvider.LongStringSuffix` — the string-tail appended to property values. Influences string-marker selection on the wire (FixStr vs StringSmall / Medium / Big), interning hit rates, and UTF-8 encode cost.
|
||||
Controls the `BenchmarkTestDataProvider.LongStringSuffix` — the string-tail appended to property values. Influences string-marker selection on the wire (FixStrAscii vs StringSmall / Medium / Big / StringAscii), interning hit rates, and UTF-8 encode cost.
|
||||
|
||||
| Profile | Content |
|
||||
|---|---|
|
||||
| `Latin1FixAscii` | Empty suffix (short FixStr fast-path stress) |
|
||||
| `Latin1Short` | "árvíztűrő tükörfúrógép" (~24 char Hungarian mixed) |
|
||||
| `Latin1Long` | ~47-char Latin1 mixed (default) |
|
||||
| `CjkBmp` | CJK BMP (3-byte UTF-8 runs) |
|
||||
| `Cyrillic` | Russian Cyrillic (2-byte UTF-8 runs) |
|
||||
| `Mixed` | Hungarian + CJK + Cyrillic + emoji (full-spectrum + surrogate pairs) |
|
||||
**Consistent length across all charsets** (UTF-16 char count): every `*Short` = 40 char, every `*Long` = 280 char (= Short × 7). Isolates the workload variable to UTF-8 byte content per charset (1-byte ASCII vs 2-byte Latin1 / Cyrillic vs 3-byte CJK vs mixed) — wire-size and encode/decode cost differences are pure charset effects, not length effects.
|
||||
|
||||
| Profile | UTF-16 char | UTF-8 byte (approx) | Tier |
|
||||
|---|---|---|---|
|
||||
| `Latin1FixAscii` | 0 | 0 | FixStrAscii / FixStr-equivalent (baseline-only) |
|
||||
| `AsciiShort` | 40 | 40 | StringAscii (167) |
|
||||
| `AsciiLong` | 280 | 280 | StringAscii (167) |
|
||||
| `Latin1Short` | 40 | ~72 | StringSmall (91) |
|
||||
| `Latin1Long` (**default**) | 280 | ~504 | StringMedium (94) |
|
||||
| `CjkBmpShort` | 40 | ~104 | StringSmall |
|
||||
| `CjkBmpLong` | 280 | ~728 | StringMedium |
|
||||
| `CyrillicShort` | 40 | ~72 | StringSmall |
|
||||
| `CyrillicLong` | 280 | ~504 | StringMedium |
|
||||
| `MixedShort` | 40 | ~88 | StringSmall |
|
||||
| `MixedLong` | 280 | ~616 | StringMedium |
|
||||
|
||||
## CLI
|
||||
|
||||
|
|
|
|||
|
|
@ -237,7 +237,10 @@ public partial class AcBinarySourceGenerator
|
|||
// These markers are feature-independent: writer emits them on any string property regardless of
|
||||
// intern setting (intern is opt-in per-property via [AcStringIntern] + InternBit).
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringSmall:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadStringSmall();");
|
||||
// FastWire mode reuses the StringSmall (=91) marker but with a different body — emit
|
||||
// inline ternary so call sites that can run in either mode (Dictionary key/value, runtime
|
||||
// cross-type populate) dispatch without an extra method-frame.
|
||||
sb.AppendLine($"{i} {a} = context.FastWire ? context.ReadStringSmallFastWire() : context.ReadStringSmallCompact();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringMedium:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadStringMedium();");
|
||||
|
|
|
|||
|
|
@ -18,33 +18,70 @@ namespace AyCode.Core.Tests.TestModels;
|
|||
/// </summary>
|
||||
public static class CharsetSuffixes
|
||||
{
|
||||
/// <summary>Empty suffix — short Hungarian baseline strings (e.g. "SharedTag") stay short, hitting
|
||||
/// the FixStr fast-path. Stress-test for FixStr / short-string code paths. Note: the baseline
|
||||
/// property values remain Hungarian; only the suffix is empty. Despite the "FixAscii" name, this
|
||||
/// option does NOT change baseline values to ASCII — it suppresses the suffix that would otherwise
|
||||
/// push every property past the FixStr boundary.</summary>
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Consistent length across all charsets (UTF-16 char count, NOT UTF-8 byte count):
|
||||
// *Short = 40 char (5-char base × 8 repetitions) → StringSmall / StringAscii tier
|
||||
// *Long = 280 char (Short × 7) → StringMedium / StringAscii tier
|
||||
//
|
||||
// Same length across charsets isolates the workload variable to UTF-8 byte content
|
||||
// (1-byte ASCII vs 2-byte Latin1 / Cyrillic vs 3-byte CJK vs mixed) — wire-size and
|
||||
// encode/decode cost differences are pure charset effects, not length effects.
|
||||
//
|
||||
// Const-concat for compile-time evaluation (usable as attribute / DataRow source).
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Empty suffix — baseline string property values stay short, hitting the
|
||||
/// <c>FixStrAscii</c> / short-string fast-path. Stress-test for short-string code paths.</summary>
|
||||
public const string Latin1FixAscii = "";
|
||||
|
||||
/// <summary>Short Latin1 mixed (Hungarian, ~24 char) — typical European i18n payload, short
|
||||
/// multi-byte runs. Below the 32-char FixStr boundary on the suffix alone, but combined with
|
||||
/// baseline values pushes every property past it.</summary>
|
||||
public const string Latin1Short = " árvíztűrő tükörfúrógép";
|
||||
// ── Pure ASCII (every byte < 0x80) ──
|
||||
// Tier: StringAscii (167) — byte→char SIMD widening, zero UTF-8 decode.
|
||||
// UTF-8 byte count: 40 byte (Short), 280 byte (Long) — 1:1 char:byte.
|
||||
private const string AsciiBase = " quic"; // 5 char ASCII
|
||||
public const string AsciiShort = AsciiBase + AsciiBase + AsciiBase + AsciiBase
|
||||
+ AsciiBase + AsciiBase + AsciiBase + AsciiBase; // 40 char
|
||||
public const string AsciiLong = AsciiShort + AsciiShort + AsciiShort + AsciiShort
|
||||
+ AsciiShort + AsciiShort + AsciiShort; // 280 char
|
||||
|
||||
/// <summary>Long Latin1 mixed (~47 char) — exceeds the 32-char FixStr boundary on the suffix alone,
|
||||
/// exercising the StringSmall+ tier path with Latin1 mixed content (Hungarian accented letters).</summary>
|
||||
public const string Latin1Long = " árvíztűrő tükörfúrógép a magyar betűzés tesztje";
|
||||
// ── Latin1 (Hungarian proxy — ISO-8859-1 + Latin-2 ő/ű) ──
|
||||
// Tier: StringSmall (91) Short / StringMedium (94) Long.
|
||||
// UTF-8 byte count: ~72 byte Short (5 char base = 9 byte UTF-8: space+á+r+v+í), ~504 byte Long.
|
||||
private const string Latin1Base = " árví"; // 5 char (space + á + r + v + í) — multi-byte mix
|
||||
public const string Latin1Short = Latin1Base + Latin1Base + Latin1Base + Latin1Base
|
||||
+ Latin1Base + Latin1Base + Latin1Base + Latin1Base; // 40 char
|
||||
public const string Latin1Long = Latin1Short + Latin1Short + Latin1Short + Latin1Short
|
||||
+ Latin1Short + Latin1Short + Latin1Short; // 280 char
|
||||
|
||||
/// <summary>CJK BMP (Chinese / Japanese / Korean Basic Multilingual Plane) — long homogeneous
|
||||
/// 3-byte UTF-8 runs. Primary win region for V4N2 Phase 3 SIMD multi-byte transcoder work.</summary>
|
||||
public const string CjkBmp = " 你好世界 こんにちは 안녕하세요";
|
||||
// ── CJK BMP (Chinese / Japanese / Korean Basic Multilingual Plane) ──
|
||||
// Tier: StringSmall (91) Short / StringMedium (94) Long.
|
||||
// UTF-8 byte count: ~104 byte Short (5 char base = 13 byte UTF-8: 1 ASCII space + 4×3-byte CJK),
|
||||
// ~728 byte Long. Homogeneous 3-byte runs — primary win region for SIMD multi-byte transcoder.
|
||||
private const string CjkBmpBase = " 你好世界"; // 5 char (space + 4 Chinese)
|
||||
public const string CjkBmpShort = CjkBmpBase + CjkBmpBase + CjkBmpBase + CjkBmpBase
|
||||
+ CjkBmpBase + CjkBmpBase + CjkBmpBase + CjkBmpBase; // 40 char
|
||||
public const string CjkBmpLong = CjkBmpShort + CjkBmpShort + CjkBmpShort + CjkBmpShort
|
||||
+ CjkBmpShort + CjkBmpShort + CjkBmpShort; // 280 char
|
||||
|
||||
/// <summary>Cyrillic (Russian / Ukrainian / etc.) — long homogeneous 2-byte runs, different shape
|
||||
/// than Hungarian mixed (where 2-byte chars are short interspersed runs).</summary>
|
||||
public const string Cyrillic = " Привет мир дорогой друг";
|
||||
// ── Cyrillic (Russian / Ukrainian) ──
|
||||
// Tier: StringSmall (91) Short / StringMedium (94) Long.
|
||||
// UTF-8 byte count: ~72 byte Short (5 char base = 9 byte UTF-8: 1 ASCII + 4×2-byte Cyrillic),
|
||||
// ~504 byte Long. Homogeneous 2-byte runs — different shape than Latin1 interspersed.
|
||||
private const string CyrillicBase = " Прив"; // 5 char (space + 4 Cyrillic)
|
||||
public const string CyrillicShort = CyrillicBase + CyrillicBase + CyrillicBase + CyrillicBase
|
||||
+ CyrillicBase + CyrillicBase + CyrillicBase + CyrillicBase; // 40 char
|
||||
public const string CyrillicLong = CyrillicShort + CyrillicShort + CyrillicShort + CyrillicShort
|
||||
+ CyrillicShort + CyrillicShort + CyrillicShort; // 280 char
|
||||
|
||||
/// <summary>Mixed full-spectrum (Hungarian + CJK + Cyrillic + emoji surrogate pairs) — multi-tier
|
||||
/// coverage in one payload. Stresses surrogate-pair handling in the UTF-8 transcoder.</summary>
|
||||
public const string Mixed = " árvíz 你好 Привет 😀";
|
||||
// ── Mixed (multi-codepage in one payload) ──
|
||||
// Tier: StringSmall (91) Short / StringMedium (94) Long.
|
||||
// UTF-8 byte count: ~88 byte Short (5 char base = 11 byte UTF-8: 1 ASCII + 1×2-byte Hungarian
|
||||
// + 1×3-byte CJK + 2×2-byte Cyrillic), ~616 byte Long. No surrogate pairs (keeps UTF-16
|
||||
// length predictable); cross-tier transcoder coverage in one payload.
|
||||
private const string MixedBase = " á你Пй"; // 5 char (space + Hungarian + Chinese + 2× Cyrillic)
|
||||
public const string MixedShort = MixedBase + MixedBase + MixedBase + MixedBase
|
||||
+ MixedBase + MixedBase + MixedBase + MixedBase; // 40 char
|
||||
public const string MixedLong = MixedShort + MixedShort + MixedShort + MixedShort
|
||||
+ MixedShort + MixedShort + MixedShort; // 280 char
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
|
|
|
|||
|
|
@ -625,30 +625,47 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// H2Q6 StringSmall reader (Compact mode): wire <c>[charLen:8][utf8Len:8][UTF-8 bytes]</c> after the
|
||||
/// marker has been consumed. 1-pass decode (no <c>CountUtf8Chars</c>). FastWire mode reuses the same
|
||||
/// marker value (=91) but a different layout — <c>[charLen:int32 LE][UTF-16 raw bytes]</c>; this method
|
||||
/// dispatches by <c>FastWire</c> flag. Single source of wire-decode shared by runtime <c>TypeReaderTable</c>,
|
||||
/// cross-type populate, AND SGen-emit.
|
||||
/// H2Q6 StringSmall reader — Compact-mode-only body: wire <c>[charLen:8][utf8Len:8][UTF-8 bytes]</c>
|
||||
/// after the marker has been consumed. 1-pass decode (no <c>CountUtf8Chars</c>).
|
||||
/// <para>Call this directly when the call site has ALREADY established <c>FastWire == false</c>
|
||||
/// (e.g. <see cref="TryReadStringProperty"/> hot path, where the SGen-emit caller short-circuits
|
||||
/// FastWire on a separate ag via <c>ReadStringUtf16Markerless</c>). Skips the redundant
|
||||
/// <c>FastWire</c> branch — call sites that may run in either mode inline
|
||||
/// <c>FastWire ? ReadStringSmallFastWire() : ReadStringSmallCompact()</c> ternary instead of a
|
||||
/// shared dispatcher (no method-frame overhead).</para>
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal string ReadStringSmall()
|
||||
internal string ReadStringSmallCompact()
|
||||
{
|
||||
if (FastWire)
|
||||
{
|
||||
// Mode-shared marker: FastWire payload is [charLen:int32 LE][UTF-16 raw bytes].
|
||||
// Fix-int charLen (matches MemPack WriteUtf16 shape) — single 4-byte read, no VarUInt loop.
|
||||
var charLenF = ReadInt32Unsafe();
|
||||
return ReadStringUtf16(charLenF);
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(!FastWire, "ReadStringSmallCompact called with FastWire=true — call sites that may run in FastWire mode must inline the `FastWire ? ReadStringSmallFastWire() : ReadStringSmallCompact()` ternary.");
|
||||
|
||||
// Compact mode — H2Q6 StringSmall: [charLen:8][utf8Len:8][bytes]
|
||||
// H2Q6 StringSmall body: [charLen:8][utf8Len:8][UTF-8 bytes]
|
||||
var header = ReadTwoBytesUnsafe();
|
||||
var charLength = (byte)header;
|
||||
var byteLength = (byte)(header >> 8);
|
||||
return ReadStringUtf8WithCharLen(charLength, byteLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// H2Q6 StringSmall reader — FastWire-mode-only body: wire <c>[charLen:int32 LE][UTF-16 raw bytes]</c>
|
||||
/// after the (mode-shared) marker has been consumed. Engaged only on the runtime
|
||||
/// <see cref="TypeReaderTable{TInput}"/> path when <c>FastWire==true</c> and the declared target
|
||||
/// type is NOT <c>string</c> (the <c>string</c>-typed FastWire short-circuit in <see cref="AcBinaryDeserializer.ReadValue{TInput}"/>
|
||||
/// bypasses the marker entirely via <c>ReadStringUtf16Markerless</c>).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal string ReadStringSmallFastWire()
|
||||
{
|
||||
// Mode-shared marker (=91) FastWire payload — fix-int charLen (matches MemPack WriteUtf16 shape).
|
||||
var charLenF = ReadInt32Unsafe();
|
||||
return ReadStringUtf16(charLenF);
|
||||
}
|
||||
|
||||
// No combined ReadStringSmall() dispatcher — every call site already has the FastWire flag
|
||||
// in scope (compile-time invariant on the SGen-emit hot path; runtime field check on the
|
||||
// dispatcher-callers). Call sites inline the ternary `FastWire ? ReadStringSmallFastWire()
|
||||
// : ReadStringSmallCompact()` when they need mode-awareness, saving a method-call frame.
|
||||
|
||||
/// <summary>
|
||||
/// H2Q6 StringMedium reader: wire <c>[charLen:16 LE][utf8Len:16 LE][UTF-8 bytes]</c> after the marker
|
||||
/// has been consumed. 1-pass decode. Header read in a single uint load (vs 2 ushort loads). Shared
|
||||
|
|
@ -788,10 +805,15 @@ public static partial class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal bool TryReadStringProperty(byte tc, out string? value)
|
||||
{
|
||||
// Hot-path invariant: SGen-emit + property-marker callers MUST short-circuit FastWire on a
|
||||
// separate ag (markerless decode) — so by the time the marker byte reaches this switch,
|
||||
// FastWire is guaranteed false. The StringSmall case therefore calls ReadStringSmallCompact
|
||||
// directly. Mode-aware call sites (Dictionary key/value emit, runtime cross-type populate,
|
||||
// TypeReaderTable lambda) inline the `FastWire ? FW : Compact` ternary themselves.
|
||||
value = null;
|
||||
switch (tc)
|
||||
{
|
||||
case BinaryTypeCode.StringSmall: value = ReadStringSmall(); return true;
|
||||
case BinaryTypeCode.StringSmall: value = ReadStringSmallCompact(); return true;
|
||||
case BinaryTypeCode.Null: return true;
|
||||
case BinaryTypeCode.StringEmpty: value = string.Empty; return true;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -98,8 +98,9 @@ public static partial class AcBinaryDeserializer
|
|||
readers[BinaryTypeCode.Decimal] = static (ctx, _) => ctx.ReadDecimalUnsafe();
|
||||
readers[BinaryTypeCode.Char] = static (ctx, _) => ctx.ReadCharUnsafe();
|
||||
// H2Q6 non-ASCII tier readers (Compact mode): fixed-width header [charLen][utf8Len] + 1-pass decode.
|
||||
// FastWire mode dispatches the StringSmall (=91) marker through the same handler — see ReadStringSmall.
|
||||
readers[BinaryTypeCode.StringSmall] = static (ctx, _) => ctx.ReadStringSmall();
|
||||
// FastWire mode reuses the StringSmall (=91) marker but with a different body — inline ternary
|
||||
// dispatches by ctx.FastWire (no method-frame overhead vs the old ReadStringSmall dispatcher).
|
||||
readers[BinaryTypeCode.StringSmall] = static (ctx, _) => ctx.FastWire ? ctx.ReadStringSmallFastWire() : ctx.ReadStringSmallCompact();
|
||||
readers[BinaryTypeCode.StringMedium] = static (ctx, _) => ctx.ReadStringMedium();
|
||||
readers[BinaryTypeCode.StringBig] = static (ctx, _) => ctx.ReadStringBig();
|
||||
readers[BinaryTypeCode.StringInterned] = static (ctx, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
|
||||
|
|
@ -146,7 +147,8 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
|
||||
// V4N5 cleanup (2026-05-06): CreateFixStrReader removed — non-ASCII short strings now use
|
||||
// StringSmall tier reader (see ReadStringSmall below).
|
||||
// StringSmall tier reader (see ReadStringSmallCompact + ReadStringSmallFastWire in
|
||||
// BinaryDeserializationContext.Read.cs).
|
||||
|
||||
/// <summary>
|
||||
/// Creates a reader for FixStrAscii with the given byte length (also char count, ASCII = 1:1).
|
||||
|
|
@ -1049,7 +1051,8 @@ public static partial class AcBinaryDeserializer
|
|||
switch (typeCode)
|
||||
{
|
||||
case BinaryTypeCode.StringSmall:
|
||||
propInfo.SetValue(target, context.ReadStringSmall());
|
||||
// FastWire reuses StringSmall (=91) marker — inline mode dispatch (no method-frame overhead).
|
||||
propInfo.SetValue(target, context.FastWire ? context.ReadStringSmallFastWire() : context.ReadStringSmallCompact());
|
||||
return true;
|
||||
case BinaryTypeCode.StringMedium:
|
||||
propInfo.SetValue(target, context.ReadStringMedium());
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,111 @@
|
|||
# BINARY — Strict SGen NuGet package (working doc)
|
||||
|
||||
Brainstorming / planning document for the **`AcBinary.Strict`** NuGet package — a SGen-only build target of the same source tree, opt-in for consumers who can guarantee `[AcBinarySerializable]` coverage and want max perf + min publish size (especially NativeAOT). Sibling to `BINARY_BYTECODE_OPTIMIZATION.md` + `BINARY_SGEN_OPTIMIZATION.md`. NOT a TODO entry — design notes.
|
||||
|
||||
## Concept
|
||||
|
||||
Two NuGet packages from the same source tree, distinguished by an MSBuild `DefineConstants`:
|
||||
|
||||
| Package | Constants | Target audience |
|
||||
|---|---|---|
|
||||
| **`AcBinary`** (default) | (none) | Drop-in JSON-like UX — 3rd-party DTOs without attributes, gradual opt-in via `[AcBinarySerializable]` on hot-paths. The **market USP**. |
|
||||
| **`AcBinary.Strict`** (opt-in) | `SGEN_ONLY` | Max perf + min AOT publish — every type must be `[AcBinarySerializable]`-decorated, Roslyn analyzer enforces at build time. |
|
||||
|
||||
**Wire format**: identical. A wire emitted by `AcBinary.Strict` is fully readable by `AcBinary` (downward compatible). The reverse holds only when the Hybrid path emitted no runtime-only markers (polymorphic `ObjectWithTypeName`, etc.) — typically true when the consumer is fully `[AcBinarySerializable]`-decorated.
|
||||
|
||||
## Market positioning — the Hybrid is NOT second-class
|
||||
|
||||
Common reason teams avoid MemPack: **kötelező attribute mindenhol**, 3rd-party DTOs nem szerializálhatók, mindent vagy semmit. The AcBinary Hybrid removes this barrier:
|
||||
- 3rd-party type → runtime reflection fallback, works out of the box
|
||||
- gradual opt-in: add `[AcBinarySerializable]` only to identified hot-paths over time
|
||||
- no big-bang refactor — coexistence of SGen + reflection types in the same graph
|
||||
|
||||
The Strict package is **the optional max-perf niche**, not a replacement. Consumers who already disciplined every DTO (`[AcBinarySerializable]` on each, no polymorphic `object` properties, no runtime-typed collections) graduate to Strict for additional AOT-perf gains.
|
||||
|
||||
## Performance gain — sources
|
||||
|
||||
Naïve estimate: "remove the runtime fallback path" — JIT inline budget freed up, ~2-5% hot path gain.
|
||||
|
||||
Realistic estimate is **larger**, because the SGen-emit itself can simplify when no runtime-cooperation is required:
|
||||
|
||||
| Aspect | Hybrid (current) | Strict-only | Gain estimate |
|
||||
|---|---|---|---|
|
||||
| Property complex-child write | `WriteObjectGenerated(value, type, ctx)` → `GetWrapper(type)` → `WriteObject` → `UseTypeReferenceHandling` check → `WriteObjectWithRefHandling/Meta/Properties` | `ChildClass_GeneratedWriter.Instance.WriteProperties(value, ctx)` direct | ~5-8 method-frames eliminated |
|
||||
| Wrapper-slot lookup | `context._wrapperSlots[s_wrapperSlot]` (array load + bounds check) | direct static field on the generated writer class | array dispatch eliminated |
|
||||
| Tracker dispatch | `wrapper.TryTrackInt32(id, visitIndex, ref ctx.NextCacheIndexRef, ...)` virtual on wrapper field | static method on the generated writer class | virtual dispatch eliminated |
|
||||
| Cold-start | `BinarySerializeTypeMetadata` ctor — reflection + Expression.Compile + GlobalMetadataCache | **eliminated entirely** | first-call latency ~50-100× faster (only JIT Tier-0 remains) |
|
||||
| Generator registry | `GeneratedWriterRegistry.TryGet(type, out writer)` dict lookup | compile-time bind → static field | dict-overhead eliminated |
|
||||
| Collection element emit | `EmitWriteCollectionElement` 4 branches (no-ref/ref/meta/both) | only 1-2 branches (no runtime-meta needed) | smaller emit IL, smaller JIT code |
|
||||
|
||||
**Realistic Ser hot-path gain: -10..-20%** (not 2-5%). Plus **NativeAOT publish size -30..-50%** (no reflection IL preserved, no `Expression.Compile`, no `IDictionary`/`IEnumerable` interface dispatch tables).
|
||||
|
||||
**Cold-start dramatic change**: first SignalR message / first cache deserialization ~100× faster on Strict (no reflection + no Expression.Compile + no metadata-dict build).
|
||||
|
||||
## Refactor prerequisite — physical separation
|
||||
|
||||
Scattered `#if !SGEN_ONLY` directives mid-method are a maintenance liability. Phase 0 (refactor-only, 0 behavior change): **physical file separation** via `partial class`:
|
||||
|
||||
```
|
||||
AcBinarySerializer.cs ← SGen + shared (always compiled)
|
||||
AcBinarySerializer.RuntimeFallback.cs ← Hybrid-only (file-level `#if !SGEN_ONLY`)
|
||||
AcBinaryDeserializer.cs ← SGen + shared
|
||||
AcBinaryDeserializer.RuntimeFallback.cs ← Hybrid-only
|
||||
AcBinaryDeserializer.TypeReaderTable.cs ← Hybrid-only (256-slot dispatch table)
|
||||
BinarySerializeTypeMetadata.cs ← shared (reduced ctor)
|
||||
BinarySerializeTypeMetadata.Reflection.cs ← Hybrid-only (reflection + Expression.Compile)
|
||||
```
|
||||
|
||||
`partial class` keeps the public surface unchanged; the body splits across files. **One `#if !SGEN_ONLY` directive per file at the top**, not 50 scattered through the method body. Bridge-methods (e.g. `WriteValueGenerated`, `ReadValueGenerated`) become `partial method` declarations — one body in `.RuntimeFallback.cs` (Hybrid), another in a `SGenBridge.cs` (Strict throws `NotSupportedException` or compile-error).
|
||||
|
||||
This refactor is **valuable on its own** — even if the Strict NuGet never ships, the codebase is clearer (Hybrid path explicit, SGen path explicit, no comingled logic).
|
||||
|
||||
## Phased implementation
|
||||
|
||||
| Phase | Scope | Risk | Behavior change |
|
||||
|---|---|---|---|
|
||||
| **0 (refactor-only)** | Physical separation: runtime-fallback code → `.RuntimeFallback.cs` partial files. Same build target, no `#if` directives yet. | very low | none |
|
||||
| **1 (Strict build target)** | New MSBuild `Configuration: SGenOnly` with `<DefineConstants>$(DefineConstants);SGEN_ONLY</DefineConstants>`. File-level `#if !SGEN_ONLY` on the runtime-only partial files. Bridge-method bodies are `partial` — Hybrid bind in `.RuntimeFallback.cs`, Strict bind throws/errors. | low | Strict consumers cannot serialize undecorated types (build / runtime error) |
|
||||
| **2 (SGen-emit simplification — the BIG perf gain)** | Generator emits direct-call pattern in Strict mode instead of bridges. `EmitDirectCollectionWrite` / `EmitReadComplex` / `EmitWriteProp` branches collapse. Slot-array dispatch replaced with static field references. | medium | hot-path perf jumps -10..-20% on Strict |
|
||||
| **3 (metadata-dict elimination)** | `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` ctor reflection paths gone in Strict. `GlobalMetadataCache` dictionary unused in Strict. Cold-start dramatic improvement. | medium | first-use latency ~100× faster on Strict |
|
||||
| **4 (Roslyn analyzer)** | Strict-mode analyzer: build-error if a property type lacks `[AcBinarySerializable]` (and isn't a known primitive), if `object`-typed property has no `[AcBinaryPolymorphic(types: typeof(A), typeof(B))]`, etc. Hybrid-mode same checks → warning only (perf hint). | low | better developer experience |
|
||||
| **5 (NuGet packaging + ADR)** | Separate `AcBinary.Strict.nupkg` publish target. ADR-level documentation. Strict-consumer guide. | low | ship to consumers |
|
||||
|
||||
Phases can be **partially shipped** — e.g. Phase 0 alone is valuable as a codebase tidy-up. Phase 1 ships the build target without performance changes. Phase 2 is the actual perf-gain. Phase 5 is the consumer-facing release.
|
||||
|
||||
## Wire-format compatibility matrix
|
||||
|
||||
| Producer | Consumer | Compatible? |
|
||||
|---|---|---|
|
||||
| AcBinary (Hybrid, fully decorated graph) | AcBinary.Strict | ✓ (no runtime-only markers emitted) |
|
||||
| AcBinary (Hybrid, with undecorated children) | AcBinary.Strict | ✗ — may contain `ObjectWithTypeName` / polymorph markers Strict doesn't decode |
|
||||
| AcBinary.Strict | AcBinary (Hybrid) | ✓ always (Strict wire ⊂ Hybrid wire) |
|
||||
| AcBinary.Strict | AcBinary.Strict | ✓ |
|
||||
|
||||
The analyzer in Phase 4 enforces the "fully decorated graph" property on Strict consumers — so the practical incompatibility (row 2) cannot occur in disciplined Strict deployments.
|
||||
|
||||
## Maintenance cost — ~10% CI overhead
|
||||
|
||||
- Source repo: single
|
||||
- Build targets: Release + SGenOnly (2× build)
|
||||
- NuGet packages: 2 (`.nupkg`)
|
||||
- Test suite: same source, conditional skip on runtime-fallback-only tests under `SGEN_ONLY` (e.g. polymorph-without-attribute tests)
|
||||
- CI time: ~2× (acceptable)
|
||||
|
||||
**Not a duplicated codebase** — same source tree, MSBuild constants differ.
|
||||
|
||||
## Open questions / considerations
|
||||
|
||||
- **Polymorph opt-in attribute**: Strict-mode requires `[AcBinaryPolymorphic(types: typeof(A), typeof(B))]` on `object`-typed properties (closed type set, compile-time bound). The Hybrid path uses runtime `value.GetType()` + `ObjectWithTypeName` — that flow doesn't exist in Strict. Spec out the attribute shape + emit pattern before Phase 4.
|
||||
- **`IDictionary<,>` / `IEnumerable<>` runtime-typed**: Strict-mode demands closed generic type arguments. Generic `Dictionary<string, object>` with polymorphic values may not be supported (or requires explicit poly-attribute on the value-type).
|
||||
- **Existing reference-handling tracker**: `wrapper.TryTrackInt32(id, ...)` is a virtual call on the wrapper. Phase 2 needs a strategy — move tracker state to `[ThreadStatic]` on the context, or generate per-type static trackers (latter more JIT-friendly).
|
||||
- **3rd-party type-coverage tooling**: Strict-consumers need a Roslyn diagnostic that lists every type in the project's type-graph that lacks `[AcBinarySerializable]` — guides the gradual migration from Hybrid to Strict.
|
||||
- **Naming**: `AcBinary.Strict` is descriptive but verbose. Alternatives: `AcBinary.Sealed`, `AcBinary.AOT`, `AcBinary.Lean`. Decision deferred to Phase 5.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `BINARY_SGEN.md` — current SGen architecture (hybrid execution model, bridge methods, wrapper slot system)
|
||||
- `BINARY_SGEN_OPTIMIZATION.md` — SGen per-property emit micro-optimization brainstorming
|
||||
- `BINARY_BYTECODE_OPTIMIZATION.md` — wire-format marker layout reorganization (orthogonal — Strict NuGet doesn't change wire format)
|
||||
- `BINARY_ISSUES.md#accore-bin-i-n6q3` — cold-start cost chain (Phase 3 addresses this directly)
|
||||
- `BINARY_TODO.md#accore-bin-t-w9f1` — compile-time metadata generation (a precondition for Phase 3 — eliminates `Expression.Compile` cold-start)
|
||||
- `BINARY_TODO.md#accore-bin-t-t5j8` — JIT Tier-1 warmup (residual cold-start after Phase 3)
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,7 @@ AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize
|
|||
- [`BINARY_SGEN.md`](BINARY_SGEN.md) — Source generator (`AyCode.Core.Serializers.SourceGenerator`)
|
||||
- [`BINARY_SGEN_OPTIMIZATION.md`](BINARY_SGEN_OPTIMIZATION.md) — SGen per-property emit micro-optimization brainstorming / methodology notes (working doc, not a TODO)
|
||||
- [`BINARY_BYTECODE_OPTIMIZATION.md`](BINARY_BYTECODE_OPTIMIZATION.md) — Wire-format hot/cold marker layout reorganization (sibling working doc, feature-flag-aligned bytecode space partition)
|
||||
- [`BINARY_STRICT_SGEN.md`](BINARY_STRICT_SGEN.md) — `AcBinary.Strict` NuGet package plan (sibling working doc, SGen-only build target, AOT-friendly opt-in next to the default Hybrid)
|
||||
- [`BINARY_ISSUES.md`](BINARY_ISSUES.md) — Known issues and limitations (binary serializer core)
|
||||
- [`BINARY_TODO.md`](BINARY_TODO.md) — Planned work / open tickets (binary serializer core)
|
||||
- [`BINARY_ASYNCPIPE_ISSUES.md`](BINARY_ASYNCPIPE_ISSUES.md) — Known issues and limitations (streaming I/O layer: `AsyncPipeReaderInput` + `AsyncPipeWriterOutput`)
|
||||
|
|
|
|||
Loading…
Reference in New Issue