diff --git a/.cr/images/0731D21DF68A684A61100DCBE1181044.png b/.cr/images/0731D21DF68A684A61100DCBE1181044.png new file mode 100644 index 00000000..38f6e803 Binary files /dev/null and b/.cr/images/0731D21DF68A684A61100DCBE1181044.png differ diff --git a/StgSharp.Common/HighPerformance/Memory/InroductionToHLSF.md b/StgSharp.Common/HighPerformance/Memory/InroductionToHLSF.md index e8a09593..3a23d32f 100644 --- a/StgSharp.Common/HighPerformance/Memory/InroductionToHLSF.md +++ b/StgSharp.Common/HighPerformance/Memory/InroductionToHLSF.md @@ -6,7 +6,7 @@ A constant-time (O(1)) allocator for real-time, high-throughput scenarios. It us - O(1) allocate/free with 10 levels: 64B x 16MB; each level split into 1x/2x/3x segments - External metadata (`Entry`, `BucketNode`) in dedicated SLABs; user blocks have no headers -- Configurable alignment (>=16B); large block cap per allocation: 32MB +- Configurable alignment (>=16B); maximum block size is configurable at construction time via the `maxBlockSize` parameter (default 32MB, 10 levels). The level array and bucket table are dynamically extended when a larger `maxBlockSize` is specified. There is no implicit fallback to `NativeMemory` for oversized requests. - Single-threaded implementation (external sync required for multi-threaded use) ## Quick Use @@ -23,13 +23,13 @@ hlsf.Free(h); ### Alloc(uint size) - Purpose: Allocate a block of at least `size` bytes. -- Input: `size` in bytes (<= 32MB for internal path; >32MB uses native large allocation). +- Input: `size` in bytes. Must not exceed the `maxBlockSize` configured at construction time (default 32MB). If `size > _maxSize`, an `InvalidOperationException` is thrown. - Output: `HybridLayerSegregatedFitAllocationHandle` with pointer access and `Span`. - Behavior: - Classify into level/segment and pop a matching bucket; escalate segments and levels if empty. - Fallback to overflow bucket or carve 32MB from spare memory if all buckets are empty. - Mark selected `Entry` as Alloc; slice any remainder into `Empty` entries and push to buckets. - - For >32MB requests, use `NativeMemory.AlignedAlloc` and mark `EntryState.Large`. + - There is no implicit fallback to `NativeMemory.AlignedAlloc` for oversized requests — all allocations are served from the pre-allocated arena. The level array and bucket table are dynamically sized based on the `maxBlockSize` parameter. ### Free(HybridLayerSegregatedFitAllocationHandle handle) @@ -47,34 +47,6 @@ hlsf.Free(h); - Consolidate fragmented regions where possible and improve bucket coverage. - Intended to be cheap; safe to call between benchmark phases. -## Benchmarks (this run) - -Environment: .NET 8 (Release), x64, single-thread, arena=3072 MiB. Results from StgSharpDebug/HlsfStressTest. - -- Free throughput: 1KB x 262,144 -> 38,928,422.9 free/s -- Alloc+free pairs: 256B x 5,000,000 -> 25,006,976.9 ops/s -- Fragmentation cycle (~512MB): fast recovery in short cycle (see test for details) - -Representative allocation throughput (higher is better): - -| Size | Ops | alloc/s (no touch) | alloc/s (touch) | -|------|----:|--------------------:|----------------:| -| 64B | 2,000,000 | 8,780,467.7 | 17,524,920.4 | -| 1KB | 1,572,864 | 8,974,865.4 | 9,585,531.6 | -| 4KB | 393,216 | 7,340,392.2 | 6,849,319.4 | -| 32KB | 49,152 | 10,376,625.6 | 7,019,407.9 | -| 256KB | 6,144 | 16,041,775.5 | 12,842,809.4 | -| 512KB | 3,072 | 20,466,355.8 | 14,195,933.5 | -| 1MB | 1,536 | 16,916,299.6 | 13,568,904.6 | -| 2MB | 768 | 17,985,948.5 | 12,569,558.1 | -| 8MB | 192 | 13,426,573.4 | 11,034,482.8 | -| 32MB | 48 | 11,162,790.7 | 9,411,764.7 | - -Notes: - -- Peak no-touch at 2MB (~17.99M alloc/s). Peak touch near 64B (~17.52M alloc/s). -- Non-aligned sizes (e.g., 768B, 3KB) typically lower due to slicing/boundary handling. - ### BenchmarkDotNet (HLSF vs Libc) Environment: .NET 8.0.23 (Release), x64 RyuJIT AVX2, Windows 11, AMD Ryzen 7 6800H. @@ -90,7 +62,7 @@ Interpretation: HLSF matches libc allocation throughput under the same workload, ## Detailed Design (summary) - -- Size classes: levelSizeArray = [64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216] (64B x 4^level) +- Size classes: The default level size array is [64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216] (10 levels, 64B × 4^level). When `maxBlockSize` is specified at construction and exceeds the default range, additional levels are appended (each 4× the previous) to cover the requested maximum block size. - Segments per level: 1x, 2x, 3x - Buckets: index = 3 * level + segment (overflow bucket for level > 9) - Metadata: Entry (external SLAB, position chain), BucketNode (user region, bucket chain) @@ -143,8 +115,8 @@ Interpretation: HLSF matches libc allocation throughput under the same workload, ### Large Allocations and Spare Memory -- Requests > 32MB use `NativeMemory.AlignedAlloc(size, align)` and mark `State.Large` -- When buckets are empty, `TryAllocateNew32MBBlock` carves a 32MB region from `_spareMemory`, builds an `Empty` entry, and advances the spare pointer +- Requests exceeding the configured `maxBlockSize` are rejected with `InvalidOperationException`. All allocations are served from the pre-allocated arena. +- When all buckets are empty, `TryAllocateNew32MBBlock` carves a region (up to 32MB) from `_spareMemory` to replenish the bucket system, builds an `Empty` entry, and advances the spare pointer ### Alignment diff --git a/StgSharp.RegularAnalysis/Abstraction/AbstractSyntaxTree.cs b/StgSharp.RegularAnalysis/Abstraction/AbstractSyntaxTree.cs index 765907f8..cde45b0f 100644 --- a/StgSharp.RegularAnalysis/Abstraction/AbstractSyntaxTree.cs +++ b/StgSharp.RegularAnalysis/Abstraction/AbstractSyntaxTree.cs @@ -181,9 +181,20 @@ namespace StgSharp.RegularAnalysis.Abstraction _state = 0; } - public void SkipNode() + /// + /// Skips the node and its subtree if the current node is the given node. Otherwise, do + /// nothing. + /// + /// + /// The node to skip. + /// + public void SkipNode( + TNode node + ) { - if (_stack.TryPop(out Record? r)) { + if (_stack.TryPeek(out Record? r) && ReferenceEquals(r.Node, node)) + { + _ = _stack.Pop(); _recycle.Push(r); } } diff --git a/StgSharp.RegularAnalysis/Abstraction/CompileStack.cs b/StgSharp.RegularAnalysis/Abstraction/CompileStack.cs index 285b8d78..a33cf88b 100644 --- a/StgSharp.RegularAnalysis/Abstraction/CompileStack.cs +++ b/StgSharp.RegularAnalysis/Abstraction/CompileStack.cs @@ -176,6 +176,7 @@ namespace StgSharp.RegularAnalysis.Abstraction #region depth + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void DecreaseDepth( ) { @@ -185,6 +186,7 @@ namespace StgSharp.RegularAnalysis.Abstraction } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void IncreaseDepth( int mark ) diff --git a/StgSharp.RegularAnalysis/Abstraction/ISyntaxNode.cs b/StgSharp.RegularAnalysis/Abstraction/ISyntaxNode.cs index 0d816001..3911e7b4 100644 --- a/StgSharp.RegularAnalysis/Abstraction/ISyntaxNode.cs +++ b/StgSharp.RegularAnalysis/Abstraction/ISyntaxNode.cs @@ -66,14 +66,6 @@ namespace StgSharp.RegularAnalysis.Abstraction TNode nextToken ); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static virtual bool IsNullOrEmpty( - TNode node - ) - { - return node is null || node.Equals(TNode.Empty); - } - void PrependNode( TNode previousNode ); diff --git a/StgSharp.RegularAnalysis/TextRegex/RegexAstNode.cs b/StgSharp.RegularAnalysis/TextRegex/RegexAstNode.cs index 9e6913f4..db926891 100644 --- a/StgSharp.RegularAnalysis/TextRegex/RegexAstNode.cs +++ b/StgSharp.RegularAnalysis/TextRegex/RegexAstNode.cs @@ -33,6 +33,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; namespace StgSharp.RegularAnalysis.TextRegex { @@ -52,10 +53,8 @@ namespace StgSharp.RegularAnalysis.TextRegex public long NodeFlag => (long)Source.Flag; - public static RegexAstNode Empty - { - get => new EmptyRegexAstNode(); - } + public static RegexAstNode Empty { get; } + = new EmptyRegexAstNode(); public RegexAstNode Previous { get; set; } @@ -68,7 +67,7 @@ namespace StgSharp.RegularAnalysis.TextRegex get; set { - field?.Parent = null; + field?.Parent = Empty; field = value; value.Parent = this; } @@ -79,7 +78,7 @@ namespace StgSharp.RegularAnalysis.TextRegex get; set { - field?.Parent = null; + field?.Parent = Empty; field = value; value.Parent = this; } @@ -90,13 +89,19 @@ namespace StgSharp.RegularAnalysis.TextRegex get => Source.Flag; } + public string Value => Source.Value; + public string CodeConvertTemplate { get; } public Token Source { get; } public void AppendNode( RegexAstNode nextToken - ) { } + ) + { + Next = nextToken; + nextToken.Previous = this; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (bool Valid,int Value) ComparePrecedence( @@ -113,9 +118,33 @@ namespace StgSharp.RegularAnalysis.TextRegex return (true,v_left - v_right); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNullOrEmpty( + RegexAstNode node + ) + { + return node is null || node.Equals(Empty); + } + public void PrependNode( RegexAstNode previousNode - ) { } + ) + { + Previous = previousNode; + previousNode.Next = this; + } + + public void RemoveChild( + RegexAstNode node + ) + { + if (Left == node) + { + Left = Empty; + } else if (Right == node) { + Right = Empty; + } + } public static bool TryMergeUnit( RegexAstNode prev, diff --git a/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.Optimize.cs b/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.Optimize.cs index 38f7211a..dc98ee78 100644 --- a/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.Optimize.cs +++ b/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.Optimize.cs @@ -28,7 +28,9 @@ using StgSharp.RegularAnalysis.Abstraction; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -43,9 +45,118 @@ namespace StgSharp.RegularAnalysis.TextRegex { TreeEnumerator enumerator = new(root); List cases = []; + RegexAstNode current; + _ = enumerator.MoveNext(); // skip root while (enumerator.MoveNext()) { - RegexAstNode current = enumerator.Current; + current = enumerator.Current; + current.Parent.RemoveChild(current); + current.Parent = RegexAstNode.Empty; + if (current.Parent.EqualityTypeConvert == RegexElementLabel.ALT) + { + continue; + } else + { + cases.Add(current); + enumerator.SkipNode(current); + } + } + for (int i = 0; i < cases.Count - 1; i++) + { + cases[i].AppendNode(cases[i + 1]); + cases[i].Parent = root; + } + return cases[0]; + } + + private static void MergeConcat( + RegexAstNode root + ) + { + if (root.EqualityTypeConvert != RegexElementLabel.CONCAT) { + return; + } + List buffer = []; + int length = 0; + RegexAstNode right = root.Right; + RegexAstNode left = root.Left; + if (left.EqualityTypeConvert != RegexElementLabel.UNIT_SPAN && + left.EqualityTypeConvert != RegexElementLabel.UNIT) { + return; + } + string str = left.Value; + buffer.Add(str); + length += str.Length; + while (RegexAstNode.IsNullOrEmpty(right) && + right.EqualityTypeConvert == RegexElementLabel.CONCAT) + { + left = right.Left; + if (left.EqualityTypeConvert != RegexElementLabel.UNIT_SPAN && + left.EqualityTypeConvert != RegexElementLabel.UNIT) + { + break; + } + str = left.Value; + buffer.Add(str); + length += str.Length; + root.Right = right.Right; + + // TODO: remove right from tree + right = root.Right; + } + str = string.Create(length, buffer, static( + span, + buffer + ) => { + int offset = 0; + for (int i = 0; i < buffer.Count; i++) + { + buffer[i].AsSpan().CopyTo(span[offset..]); + offset += buffer[i].Length; + } + }); + left = root.Left; + root.Left = new RegexAstNode(new Token(str, left.Source.Line, + left.Source.Column, + str.Length == 1 ? + RegexElementLabel.UNIT : + RegexElementLabel.UNIT_SPAN)); + } + + private static void RotateConcat( + RegexAstNode root + ) + { + RegexAstNode left = root.Left; + while (RegexAstNode.IsNullOrEmpty(left) && + left.EqualityTypeConvert == RegexElementLabel.CONCAT) + { + /* + * root → root + * ╱ ╲ ╱ ╲ + * left c a left + * ╱ ╲ ╱ ╲ + * a b b c + */ + RegexAstNode a = left.Left; + RegexAstNode b = left.Right; + RegexAstNode c = root.Right; + root.Left = a; + if (RegexAstNode.IsNullOrEmpty(a)) { + a.Parent = root; + } + + root.Right = left; + left.Left = b; + left.Right = c; + if (RegexAstNode.IsNullOrEmpty(b)) { + b.Parent = left; + } + + if (RegexAstNode.IsNullOrEmpty(c)) { + c.Parent = left; + } + left = root.Left; } } diff --git a/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.cs b/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.cs index ca8e038e..15ad6e3b 100644 --- a/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.cs +++ b/StgSharp.RegularAnalysis/TextRegex/RegexInterpreter.cs @@ -174,24 +174,37 @@ namespace StgSharp.RegularAnalysis.TextRegex { // phase0: scan container and remove no parent nodes _tree.AllNodes.RemoveWhere(node => node.Parent == null && node != _tree.Root); - HashSet.Enumerator enumerator = _tree.AllNodes.GetEnumerator(); + TreeEnumerator enumerator = new TreeEnumerator(_tree.Root); + RegexAstNode current; // phase1: ALT flatten + // phase2: CONCAT normalize while (enumerator.MoveNext()) { - RegexAstNode node = enumerator.Current; - if ((RegexElementLabel)node.NodeFlag == RegexElementLabel.ALT) + current = enumerator.Current; + if (current.EqualityTypeConvert == RegexElementLabel.ALT) { - RegexAstNode list = FlattenAlt(node); - node.Left = list; - node.Right = RegexAstNode.Empty; + RegexAstNode union = FlattenAlt(current); + current.Right = union; + current.Left = RegexAstNode.Empty; + } else if (current.EqualityTypeConvert == RegexElementLabel.CONCAT) { + RotateConcat(current); } } - // phase2: ALT merge - - // phase3: CONCAT merge + enumerator.Reset(); + while (enumerator.MoveNext()) + { + // suppose all concat is now in normalized form, + // we can merge adjacent unit nodes into a single unit node with string value + current = enumerator.Current; + if (current.EqualityTypeConvert == RegexElementLabel.CONCAT) { + MergeConcat(current); + } + } + + // phase4: ALT union } private void ProcessOperator( diff --git a/StgSharp.RegularAnalysis/TextRegex/RegexTokenReader.cs b/StgSharp.RegularAnalysis/TextRegex/RegexTokenReader.cs index af43e523..68922948 100644 --- a/StgSharp.RegularAnalysis/TextRegex/RegexTokenReader.cs +++ b/StgSharp.RegularAnalysis/TextRegex/RegexTokenReader.cs @@ -65,7 +65,7 @@ namespace StgSharp.RegularAnalysis.TextRegex } position = pos + 2; return new Token(_source[(pos)..(pos + 2)], 0, pos, - RegexElementLabel.UNIT); + RegexElementLabel.UNIT_SET); case '[': for (int i = pos + 1; i < _source.Length; i++) {