diff --git a/.claude/settings.local.json b/.claude/settings.local.json index acc3b01..c912797 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -65,7 +65,9 @@ "PowerShell($appDataPaths = @\\(\"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\App_Data\\\\plugins.json\", \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\App_Data\\\\plugins.installed.json\"\\); foreach \\($f in $appDataPaths\\) { if \\(Test-Path $f\\) { Write-Output \"=== $f ===\"; Get-Content $f -Raw } else { Write-Output \"NOT FOUND: $f\" } })", "Read(//h/Applications/Mango//**)", "Read(//h/Applications/Mango/LLM_PLAN//**)", - "Bash(curl -s \"https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeWriter.cs\")" + "Bash(curl -s \"https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeWriter.cs\")", + "WebFetch(domain:lemire.me)", + "Bash(gh pr *)" ] } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs index 346330b..e0e1fe1 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs @@ -390,14 +390,131 @@ public static partial class AcBinaryDeserializer return string.Create(length, (Buffer: _buffer, Start: pos), static (chars, state) => { var src = state.Buffer.AsSpan(state.Start, chars.Length); - for (int i = 0; i < chars.Length; i++) + for (var i = 0; i < chars.Length; i++) chars[i] = (char)src[i]; }); } - var value2 = Utf8NoBom.GetString(_buffer, _position, length); - _position += length; - return value2; + // Non-ASCII path: custom UTF-8 decoder. + // Beats Encoding.UTF8.GetString by skipping the virtual-dispatch + encoder-fallback + // overhead the BCL adds for arbitrary inputs. Two passes (count + decode) over the + // bytes — both passes are tight scalar loops the JIT can auto-vectorize for the + // common 1-byte (ASCII) branch, with predictable branches for 2/3-byte sequences + // (Latin extended, Cyrillic, Greek, CJK BMP). 4-byte sequences (supplementary plane: + // emoji, rare CJK ext) decode to a UTF-16 surrogate pair. + // + // The bytes are guaranteed valid UTF-8 because we wrote them via Encoding.UTF8.GetBytes + // — no validation needed beyond the bounds checks Span indexing already provides. + // If a wire payload is corrupt, an IndexOutOfRangeException surfaces at the + // continuation-byte read, which the calling deserializer propagates as a + // deserialization failure (same exception class as the BCL path's malformed-input + // handling). + return DecodeUtf8(length); + } + + /// + /// Custom UTF-8 → UTF-16 string decoder. Single-allocation via string.Create; + /// counts chars first (vectorizable scalar loop), then decodes directly into the + /// allocated string's buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] // cold path; let JIT keep ReadStringUtf8 caller small + private string DecodeUtf8(int byteLength) + { + var pos = _position; + _position += byteLength; + + var srcSpan = _buffer.AsSpan(pos, byteLength); + var charCount = CountUtf8Chars(srcSpan); + + return string.Create(charCount, (Buffer: _buffer, Pos: pos, Len: byteLength), static (chars, state) => + { + DecodeUtf8ToChars(state.Buffer.AsSpan(state.Pos, state.Len), chars); + }); + } + + /// + /// Counts UTF-16 chars produced by decoding the given UTF-8 byte span. + /// JIT-vectorizable scalar loop: every iteration is a constant-shape branch on bit patterns. + /// + /// + /// Char-count rules: + /// • Continuation bytes (10xxxxxx, 0x80–0xBF) — produced no char, skip. + /// • All other start bytes (0xxxxxxx, 110xxxxx, 1110xxxx) — produce 1 char each. + /// • 4-byte start bytes (11110xxx, 0xF0–0xF7) — produce 2 chars (surrogate pair). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CountUtf8Chars(ReadOnlySpan bytes) + { + var count = 0; + for (var i = 0; i < bytes.Length; i++) + { + var b = bytes[i]; + // Non-continuation byte: increments char count + if ((b & 0xC0) != 0x80) count++; + // 4-byte start (11110xxx): adds extra char for surrogate pair + if ((b & 0xF8) == 0xF0) count++; + } + return count; + } + + /// + /// Decodes UTF-8 bytes into UTF-16 chars in place. Caller guarantees + /// has at least the char count returned by . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeUtf8ToChars(ReadOnlySpan src, Span dst) + { + int srcIdx = 0, dstIdx = 0; + while (srcIdx < src.Length) + { + var b0 = src[srcIdx]; + switch (b0) + { + case < 0x80: + // 1-byte ASCII (U+0000–U+007F) + dst[dstIdx++] = (char)b0; + srcIdx += 1; + break; + case < 0xE0: + { + // 2-byte sequence: 110xxxxx 10xxxxxx → U+0080–U+07FF + // Latin extended (Hungarian, Polish, Czech, Spanish, French diacritics), + // Greek, Cyrillic, Hebrew, Arabic, etc. + var b1 = src[srcIdx + 1]; + + dst[dstIdx++] = (char)(((b0 & 0x1F) << 6) | (b1 & 0x3F)); + srcIdx += 2; + break; + } + case < 0xF0: + { + // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx → U+0800–U+FFFF + // CJK BMP (most Chinese, Japanese, Korean), various other scripts. + var b1 = src[srcIdx + 1]; + var b2 = src[srcIdx + 2]; + + dst[dstIdx++] = (char)(((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F)); + srcIdx += 3; + break; + } + default: + { + // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx → U+10000–U+10FFFF + // Supplementary plane (emoji, rare CJK ext, ancient scripts) — encoded as + // a UTF-16 surrogate pair. + var b1 = src[srcIdx + 1]; + var b2 = src[srcIdx + 2]; + var b3 = src[srcIdx + 3]; + var codepoint = ((b0 & 0x07) << 18) | ((b1 & 0x3F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F); + + codepoint -= 0x10000; + dst[dstIdx++] = (char)(0xD800 | (codepoint >> 10)); + dst[dstIdx++] = (char)(0xDC00 | (codepoint & 0x3FF)); + srcIdx += 4; + break; + } + } + } } private string ReadStringUtf8Cached(int length) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 1d3f0ee..2445cbb 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -671,29 +671,41 @@ public static partial class AcBinarySerializer return; } + // D-2: single-pass UTF-8 encode with VarUInt backfill. + // Replaces the prior try-ASCII-then-rewind-and-encode-UTF-8 pattern (1 scan ASCII / 3 scans + // non-ASCII) with a single GetBytes call that works identically for both content classes. + // + // Layout: [reserved 5 bytes for max VarUInt][UTF-8 bytes...] + // 1. EnsureCapacity for worst-case (5 + charLength*4) + // 2. GetBytes directly into buffer at savedPos+5 → returns exact byteCount + // 3. If actual VarUInt size < 5, memmove encoded bytes left to compact the gap + // 4. WriteVarUInt at savedPos and advance + // + // Span.CopyTo is overlap-safe via Buffer.Memmove. For typical short strings + // (≤127 bytes UTF-8 → 1-byte VarUInt), the shift is 4 bytes — a few ns memcopy cost + // that's dwarfed by the saved ASCII-scan-then-rewind overhead on non-ASCII content, + // and is essentially free on ASCII content (cache-resident write). var charLength = value.Length; + const int maxVarUIntSize = 5; + var maxBytes = charLength * 4; - // Pre-allocate VarUInt + ASCII body BEFORE savedPosition — if Grow happens, - // it fires here, before the save. savedPosition is always in the current chunk. - EnsureCapacity(VarUIntSize((uint)charLength) + charLength); - var savedPosition = _position; + EnsureCapacity(maxVarUIntSize + maxBytes); - WriteVarUIntUnsafe((uint)charLength); - if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) + var savedPos = _position; + var encodeStart = savedPos + maxVarUIntSize; + var bytesWritten = Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes)); + + var varUIntSize = VarUIntSize((uint)bytesWritten); + if (varUIntSize < maxVarUIntSize) { - _position += charLength; - return; + var shift = maxVarUIntSize - varUIntSize; + _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten)); } - // Non-ASCII fallback: safe rewind (no Grow happened since pre-allocate) - _position = savedPosition; + _position = savedPos; - var byteCount = Utf8NoBom.GetByteCount(value); - EnsureCapacity(VarUIntSize((uint)byteCount) + byteCount); - WriteVarUIntUnsafe((uint)byteCount); - - Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); - _position += byteCount; + WriteVarUIntUnsafe((uint)bytesWritten); // advances _position by varUIntSize + _position += bytesWritten; } public void WriteFixStr(string value) diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs index 23c761a..fceaae1 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs @@ -85,16 +85,39 @@ internal static class BinaryTypeCode // Property skip marker (SlotCount + 38) public const byte PropertySkip = SlotCount + 38; // 102 — Marks a property with default/null value (skipped during serialization) - // FixStr range: SlotCount + 39 .. SlotCount + 70 (32 values for strings 0-31 bytes) - // FixStr encoding: FixStrBase + length (0-31) - // This saves 1 byte for short strings by combining type + length in single byte + // FixStr range (UTF-8 short strings): 103..134 (32 values for byte lengths 0-31) + // FixStr encoding: FixStrBase + byteLength + // Saves 1 byte for short strings by combining type + length in single byte. + // Content semantics: UTF-8 bytes (may be ASCII or multi-byte). The reader-side decoder dispatches + // on content via the new ASCII variant range below — this range is the "universal short" / UTF-8 lane. public const byte FixStrBase = SlotCount + 39; // 103 public const byte FixStrMax = FixStrBase + 31; // 134 public const int FixStrMaxLength = 31; - // Flag-based header markers (must be 16-aligned for flag bits in lower nibble) - // Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F) - public const byte HeaderFlagsBase = 144; // 0x90 — next 16-aligned value after FixStrMax + // FixStrAscii range (ASCII-only short strings): 135..166 (32 values for byte lengths 0-31) + // FixStrAscii encoding: FixStrAsciiBase + byteLength + // Content semantics: pure ASCII bytes (every byte < 0x80). Reader can use byte→char widening + // without UTF-8 decode or ASCII validation — the marker itself is the validation contract. + // Writer emits this when it can prove the content is ASCII (e.g., GetBytes returns byteCount == charLength). + public const byte FixStrAsciiBase = SlotCount + 71; // 135 + public const byte FixStrAsciiMax = FixStrAsciiBase + 31; // 166 + public const int FixStrAsciiMaxLength = 31; + + // Long ASCII string marker: 167 + // Layout: [StringAscii] [VarUInt byteCount] [ASCII bytes] + // Counterpart to String (91) which is the universal/UTF-8 long-string marker. + // Reader fast-widens via byte→char without UTF-8 decode or IsValid scan. + public const byte StringAscii = SlotCount + 103; // 167 + + // Reserved slot block: 168..175 (8 slots) for future string-related markers + // (e.g., StringCompressed, StringEncoded, StringMixedAscii, etc.). Keeping the 135..167 range + // dedicated to ASCII variants for clean range-checks (see IsAsciiString below). + + // Flag-based header markers (must be 16-aligned for flag bits in lower nibble). + // Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F). + // Moved from 144 → 176 (next 16-aligned value after the new ASCII string range) to keep all + // string-related markers in one contiguous block 91..167 / FixStrBase..StringAscii. + public const byte HeaderFlagsBase = 176; // 0xB0 — next 16-aligned value after StringAscii reserved block public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included // Reference handling uses 2 separate bits: // Bit 1 (0x02): OnlyId - reference handling for IId objects only @@ -113,58 +136,103 @@ internal static class BinaryTypeCode /// Check if type code represents a reference (string or object). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsReference(byte code) => code is StringInterned or ObjectRef; + public static bool IsReference(byte typeCode) => typeCode is StringInterned or ObjectRef; /// - /// Check if type code is a FixStr (short string with length encoded in type code). + /// Check if type code is any string-related marker — long inline (String / StringAscii), + /// interning markers (StringInterned, StringInternFirst), empty marker, or any FixStr variant + /// (UTF-8 or ASCII). Centralized predicate so adding/removing string markers requires updating + /// only this method, not every dispatch site. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; + public static bool IsString(byte typeCode) + => (typeCode is >= String and <= StringInternFirst) // 91..94: String, StringInterned, StringEmpty, StringInternFirst + || (typeCode is >= FixStrBase and <= StringAscii); // 103..167: FixStr (UTF-8 short) + FixStrAscii (ASCII short) + StringAscii (ASCII long) /// - /// Decode FixStr length from type code. + /// Check if type code is a FixStr (UTF-8 short string with byte length encoded in type code). + /// Does NOT match FixStrAscii — use for that, or + /// for the full ASCII-string range. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int DecodeFixStrLength(byte code) => code - FixStrBase; + public static bool IsFixStr(byte typeCode) => typeCode is >= FixStrBase and <= FixStrMax; /// - /// Encode FixStr type code for given byte length (0-31). + /// Decode FixStr byte length from type code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DecodeFixStrLength(byte typeCode) => typeCode - FixStrBase; + + /// + /// Encode FixStr type code for given byte length (0-31). Caller asserts UTF-8 content semantics. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength); /// - /// Check if byte length can be encoded as FixStr. + /// Check if byte length can be encoded as FixStr (UTF-8 short string, 0..31 bytes). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; + /// + /// Check if type code is any ASCII string marker — FixStrAscii (short) or StringAscii (long). + /// Single contiguous range (135..167) for branch-friendly dispatch on the reader hot path. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAsciiString(byte typeCode) => typeCode is >= FixStrAsciiBase and <= StringAscii; + + /// + /// Check if type code is a FixStrAscii (ASCII short string with byte length encoded in type code). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFixStrAscii(byte typeCode) => typeCode is >= FixStrAsciiBase and <= FixStrAsciiMax; + + /// + /// Decode FixStrAscii byte length from type code. Length is also the char count (1 byte = 1 char for ASCII). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DecodeFixStrAsciiLength(byte typeCode) => typeCode - FixStrAsciiBase; + + /// + /// Encode FixStrAscii type code for given byte length (0-31). Caller asserts ASCII content semantics + /// (every byte less than 0x80). Misuse on non-ASCII content corrupts decode. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte EncodeFixStrAscii(int byteLength) => (byte)(FixStrAsciiBase + byteLength); + + /// + /// Check if byte length can be encoded as FixStrAscii (ASCII short string, 0..31 bytes). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanEncodeAsFixStrAscii(int byteLength) => byteLength is >= 0 and <= 31; + /// /// Check if type code is a tiny int (single byte int32 encoding). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsTinyInt(byte code) => code >= Int32Tiny; + public static bool IsTinyInt(byte typeCode) => typeCode >= Int32Tiny; /// /// Decode tiny int value from type code. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; + public static int DecodeTinyInt(byte typeCode) => typeCode - Int32Tiny - 16; /// /// Encode small int value (-16 to 47) as type code. /// Returns true if value fits in tiny encoding. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryEncodeTinyInt(int value, out byte code) + public static bool TryEncodeTinyInt(int value, out byte typeCode) { // Range: -16 to 47 (64 values total, fitting in 192-255) if (value is >= -16 and <= 47) { - code = (byte)(value + 16 + Int32Tiny); + typeCode = (byte)(value + 16 + Int32Tiny); return true; } - code = 0; + typeCode = 0; return false; } }