remove useless node from source gen pipeline. add source gen logic

This commit is contained in:
Youmu Liu
2026-08-21 18:31:31 +08:00
parent 968a6afe1a
commit f0b6a4038e
15 changed files with 219 additions and 334 deletions
@@ -124,24 +124,6 @@ namespace StgSharp.Collections
_index += span.Length;
}
public void PushRange(
SpanAction<T, int> filling,
int count
)
{
if (count == 0) {
return;
}
if (_index + count > _values.Length) {
throw new InvalidOperationException("Not enough space in the stack to push the entire span.");
}
if (filling is not null)
{
filling(_values.AsSpan()[_index..(_index + count)], count);
_index += count;
}
}
public bool TryPop(
out T value
)
@@ -157,6 +139,18 @@ namespace StgSharp.Collections
return true;
}
internal void FillRange(
SpanAction<T, nuint> filling,
nuint buffer
)
{
if (filling is not null)
{
filling(_values.AsSpan()[_index..], buffer);
_index = _values.Length;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Span<T> GetValueLayout()
{
@@ -1,159 +0,0 @@
# TLSF (Two-Layer Segregated Fit) Allocator
This document describes the current StgSharp native arena allocator, `StgSharp.HighPerformance.Memory.TwoLayerSegregatedFitAllocator`. The file name is historical; the implementation described below is TLSF rather than the older HLSF allocator.
## Overview
- Single contiguous native arena allocated up front
- Headerless returned blocks; allocator metadata is stored in 32-byte nodes carved from the tail of the same arena
- Requests rounded up to 64-byte granularity, with 64 bytes as the minimum block size
- Free blocks grouped by coarse size level derived from the highest set bit of the block size
- Exact-level allocation uses first-fit traversal inside the selected bucket; higher levels fall back to the head of the first non-empty bucket
- `Free` performs bounded local coalescing; `Collect` performs full free-run coalescing across the arena
- Single-threaded implementation; concurrent access requires external synchronization
- No arena growth and no implicit fallback to `NativeMemory.AlignedAlloc` for oversized requests
## Quick Use
```csharp
using StgSharp.HighPerformance.Memory;
using var tlsf = new TwoLayerSegregatedFitAllocator(64 * 1024 * 1024);
var handle = tlsf.Alloc(1024);
Span<byte> buffer = handle.AsSpan();
buffer[0] = 42;
tlsf.Free(handle);
```
## Constructors
- `TwoLayerSegregatedFitAllocator(nuint byteSize)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, uint maxBlockSize)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, int align)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, int align, nuint maxBlockSize)`
Behavior summary:
- `byteSize` defines the total arena size
- `maxBlockSize` defaults to 32 MB and defines the largest single request accepted by `Alloc` and `TryAlloc`
- `align` is passed to the backing `NativeMemory.AlignedAlloc` call for arena allocation; request sizes inside the arena are still quantized to 64-byte blocks
## Public API
### `Alloc(nuint size)`
- Rounds `size` up to the next 64-byte block, with a minimum of 64 bytes
- Throws `OverflowException` if the normalized size exceeds `maxBlockSize` or if the arena cannot provide a block
- Returns a `Handle` containing block address and block size
- Splits a sufficiently large tail remainder into a new free node and re-buckets that node
### `TryAlloc(nuint size, out Handle handle)`
- Uses the same size normalization and maximum block limit as `Alloc`
- Returns `false` when the request exceeds the configured limit or when the arena is exhausted
- Does not fall back to the system allocator
### `Free(Handle handle, int collectScanWindow = 4)`
- Marks the block as free
- Scans up to `collectScanWindow` neighbors toward lower addresses and then up to `collectScanWindow` neighbors toward higher addresses
- Merges any contiguous free neighbors found during those scans
- Reclassifies the merged block back into the coarse bucket set
### `Collect()`
- Walks the full position chain from low address to high address
- Merges consecutive free runs without the local scan-window limit used by `Free`
- Restores larger reusable spans and improves bucket coverage after fragmentation-heavy phases
### `Dispose()`
- Releases the backing arena through the stored free callback
### `Handle`
- Readonly value type carrying the allocator metadata reference, block address, and block size
- `Address` exposes the native pointer
- `Size` exposes the normalized block size
- `AsSpan(int range = -1)` exposes a span view over the block
- The indexer provides byte-level access by offset
## Allocation Model
### Level Classification
- `GetLevel(size)` maps the normalized size to `57 - BitOperations.LeadingZeroCount(size)` and clamps the result to `_maxLevel`
- Each bucket corresponds to one coarse size class rather than an exact size
- The target bucket can therefore contain blocks of different sizes within the same coarse level
### Allocation Path
1. Normalize the requested size to a 64-byte multiple.
2. Traverse the target bucket and select the first free block whose size is at least the requested size.
3. If the target bucket has no suitable block, pop the head of the first non-empty higher-level bucket.
4. If all buckets are empty, carve a fresh block from the spare region tracked by the arena sentinel.
5. Split the tail remainder when both remainder size and metadata availability allow it.
### Metadata Layout
- `Metadata` is a 32-byte node
- `NextNear` and `PreviousNear` form the address-ordered position chain
- `NextLevel` and `PreviousLevel` form either the bucket chain or the recycled-metadata chain, depending on node state
- The spare sentinel at the end of the arena tracks remaining virgin space, recycled metadata nodes, and position-chain anchors
- Returned blocks remain headerless from the caller's point of view
### Coalescing Model
- `Free` performs bounded local coalescing to keep the hot path short
- `Collect` performs full coalescing of consecutive free runs across the entire position chain
- Recycled metadata nodes are returned to the spare metadata list and reused by later splits or fresh arena chunks
## Complexity and Runtime Characteristics
- `Alloc`: O(1) bucket index calculation plus target-level first-fit traversal; higher-level fallback is O(1)
- `Free`: O(`collectScanWindow`) local merge cost
- `Collect`: O(number of metadata nodes in the position chain)
- Best steady-state behavior appears on repeated small and medium native allocations inside a pre-allocated arena
## BenchmarkDotNet (TLSF vs Libc)
Environment: .NET 8.0.26 (PerformanceDebug), x64 RyuJIT AVX2, Windows 11, AMD Ryzen 7 6800H, BenchmarkDotNet v0.15.8, InProcessEmitToolchain, LaunchCount=1, WarmupCount=6, IterationCount=15, InvocationCount=3.
Workload summary:
- Arena size: 512 MB
- Arena alignment: 64 bytes
- Allocation count per batch: 2048
- Batch count: 256
- Request set before normalization: 64, 100, 200, 400, 1024, 4096 bytes
- Free order: shuffled per batch
- `Free` local merge window: 4
Best observed run under the current harness:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|------- |-----:|------:|-------:|------:|--------:|----------:|------------:|
| Tlsf | 79.11 ns | 5.944 ns | 5.560 ns | 0.65 | 0.09 | - | NA |
| Libc | 123.64 ns | 15.955 ns | 14.144 ns | 1.01 | 0.16 | - | NA |
Worst observed run under the same harness:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|------- |-----:|------:|-------:|------:|--------:|----------:|------------:|
| Tlsf | 82.95 ns | 6.337 ns | 5.617 ns | 0.81 | 0.07 | - | NA |
| Libc | 102.63 ns | 8.590 ns | 7.173 ns | 1.00 | 0.09 | - | NA |
Interpretation:
- The current single-pass bucket selection fast path materially reduces allocator-side latency
- The reported ratio still drifts across repeated compile-and-run cycles because the libc baseline also moves under the current InProcess harness
- Recent repeated runs place the transient best case near 0.65 and the longer-running steady band near 0.75 to 0.80
## Operational Notes
- Maximum single-allocation size is fixed at construction time
- Arena growth is not implemented
- `Collect` is the global defragmentation pass; `Free` only performs bounded local coalescing
- The allocator is intended for explicit native-memory workflows rather than as a drop-in general-purpose heap replacement
@@ -1,762 +0,0 @@
# L4 Software Cache Design Specification
> Project: StgSharp
> Author: Nitload
> Version: Draft v0.3
> Date: 2026-03-10
---
## 1. Purpose
### 1.1 Problem Background
StgSharp's matrix computation system is migrating from a "single global context table" to a
"per-thread context" architecture to adapt to the scheduling characteristics of hybrid-architecture
CPUs (e.g., Intel P-Core / E-Core). After the migration, each thread independently holds its own
computation context, but matrix data may still be allocated by the HLSF allocator or global
`malloc` on a remote NUMA node, resulting in significantly increased cross-node access latency.
At the same time, in matrix multiplication (FMA), the core row data of matrix A in the
checkerboard-shaped kernel grid layout must be repacked (Packed) into a micro-kernel-friendly
contiguous arrangement. In libraries such as OpenBLAS, this Pack operation is re-executed on
every tile switch, whereas L4 can cache the Pack results to avoid repeated reordering.
Additionally, the segmented accumulation (segment) of matrix multiplication requires storing
intermediate partial sums locally to avoid cross-NUMA writes to the target matrix on every
segment. L4 can serve as a local accumulation buffer for partial sums, writing them back in a
single operation after all segments complete locally.
### 1.2 Solution
The L4 cache is a purely software-implemented **data caching layer** logically positioned
between the CPU hardware caches (L1/L2/L3) and main memory. Its core responsibilities:
1. **Pack result caching**: Cache the repack results of FMA core rows in matrix A to avoid repeated Pack operations
2. **NUMA migration cache**: Migrate data from remote NUMA nodes to local memory and cache it, eliminating cross-node latency
3. **Segment accumulation buffer**: Temporarily store partial sums from FMA segmented computation to reduce remote write operations
4. **PLU intermediate data buffer**: Provide a local contiguous workspace for PLU decomposition
### 1.3 Design Goals
- Provide NUMA-friendly local data access for per-thread matrix computation contexts
- Fixed capacity (64 MB), no runtime expansion, deterministic latency
- Transparent pointer redirection: upper-layer code queries via original pointers; L4 returns local cache pointers
- Cache eviction policy based on **RRPV mask + windowed OPT prediction**
- Support both **StreamTransparent** and **CacheBuffer** working modes
- Support **writable cache lines** that invoke registered callback functions for pre-processing and write-back upon eviction or explicit flush
---
## 2. Overall Architecture
### 2.1 Three-Component Model
```
┌���─────────────────────────────────────────────────────────────────┐
│ L4 │
│ │
│ ┌──────────────┐ ┌────────────────────┐ ┌──────────────────┐ │
│ │ CacheLine │ │ L4 Core │ │ Predictor │ │
│ │ Handle │ │ Cache │ │ Prefetch │ │
│ │ │ │ │ │ Predictor │ │
│ │ • local addr │ │ • Swiss Table │ │ • 2048-entry buf │ │
│ │ • ref count │ │ • Slab Allocator │ │ • threshold 1024 │ │
│ │ • Dispose │ │ • RRPV mask │ │ • windowed OPT │ │
│ │ drops ref │ │ • SIMD eviction │ │ • active key set │ │
│ │ │ │ • writeback mgmt │ │ │ │
│ └──────────────┘ └────────────────────┘ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 2.2 Data Access Path
```
Compute thread accessing data:
┌─────────────────────────────────────────────────────────┐
│ Read-only path: L4.Map(originKey) │
│ ├─ StreamTransparent + no repack needed? │
│ │ └─ Yes → return original pointer directly │
│ │ (zero overhead) │
│ │ │
│ └─ CacheBuffer mode │
│ ├─ Swiss Table lookup (2.6ns, lock-free) │
│ ├─ Hit → update RRPV=0, return CacheLine handle │
│ └─ Miss → SpinWait for Predictor prefetch │
├─────────────────────────────────────────────────────────┤
│ Writable path: L4.MapWritable(originKey, preprocess, writeback) │
│ ├─ Hit → return existing partial sum cache line │
│ └─ Miss → allocate zero-initialized cache line, │
│ register writeback callbacks │
│ cache line marked dirty (dirty=1) │
│ eviction invokes preprocess + writeback │
├─────────────────────────────────────────────────────────┤
│ After computation: │
│ 3. CacheLine.Dispose() (refCount--) │
│ 4. L4.StepComplete() │
│ ├─ Predictor.AdvanceExecutionPoint() │
│ └─ Predictor.NeedsPrefetch? │
│ └─ Yes → CAS-acquire lock, execute prefetch │
└─────────────────────────────────────────────────────────┘
```
### 2.3 Key Parameters
| Parameter | Value | Description |
|------------------------|--------------------------|------------------------------------------------------|
| Total cache capacity | 64 MB | Per-NUMA-node contiguous memory |
| Cache line size | 8192 Bytes | = 64 × `double MAT_KERNEL` |
| Cache line count | 8192 | 64MB / 8192B |
| Mapping table type | Swiss Table | 1024 buckets × 16 slots, SIMD-accelerated |
| Cache line pool | Slab Allocator | Non-concurrent, fixed capacity, no auto-expansion |
| Eviction policy | RRPV mask + windowed OPT | SoA layout + SSE2 batch scan |
| Prefetch policy | Predictor windowed OPT | 2048-entry buffer, 1024-step lookahead |
| Writeback policy | Callback-driven | Invokes registered callbacks on dirty eviction/flush |
---
## 3. Key Components
### 3.1 Swiss Table (Pointer Mapping Table)
**Responsibility**: Maintains the `original pointer → cache pointer` mapping.
- Key: original pointer `nint`, pointing to data allocated by HLSF/malloc on a remote node
- Value: cache pointer `nint`, pointing to locally Slab-allocated cache line
- Hash: hardware `CRC32C` instruction on the 64-bit pointer
- Low 10 bits → bucket index (1024 buckets)
- Bits 10-16 → H2 control byte (used for SIMD pre-filtering)
- Scan: SSE2 `pmovmskb` instruction for parallel comparison of 16 control bytes
- Tombstone mechanism: deleted entries set ctrl to `0xFE`, preserving probe chain integrity
- Capacity: 1024 × 16 = 16,384 mappings, significantly larger than total cache line count (8192 lines),
ensuring extremely low hash collision rate
**Measured Performance** (Ryzen 7 6800H, .NET 8, BenchmarkDotNet):
| Operation | Swiss Table | Dictionary | Speedup |
|--------------------------|-------------|-------------|---------|
| Zipfian Lookup (8192×) | 20.0 μs | 76.1 μs | 3.8× |
| Uniform Lookup (8192×) | 18.6 μs | 50.6 μs | 2.7× |
| Insert (4096 entries) | 18.5 μs | 46.5 μs | 2.5× |
| SteadyState (4096 ops) | 25.3 μs | 68.3 μs | 2.7× |
Single lookup latency ≈ 2.6 ns, GC allocation = 0.
### 3.2 Slab Allocator (Cache Line Pool)
**Responsibility**: Manages allocation and reclamation of fixed-size (8192B) cache lines.
- Mode: **Non-concurrent, fixed capacity** (`FixedSlabAllocator<T>` mode),
reusing the existing `SlabAllocator.FromBuffer<T>()` interface
- No auto-expansion: capacity is fixed at initialization (64MB / 8192B = 8192 lines);
allocation failure triggers eviction rather than expansion
- Allocation complexity: O(1), pops from free stack
- Reclamation complexity: O(1), pushes back to free stack
- Alignment: all cache lines are 64B aligned, ensuring no cross-hardware-cache-line boundaries
### 3.3 Eviction Metadata (SoA Layout + RRPV Mask + Writeback Callbacks)
**Responsibility**: Tracks per-cache-line reference state, heat, and writeback information to determine eviction priority.
Replaces the four-level circular linked list from v0.1 with parallel arrays in Structure-of-Arrays layout:
```
SoA Layout — Parallel Arrays
_refFlags[i] : byte Reference count snapshot (0=free, non-zero=held)
_evictionFlags[i] : byte dirty(1bit) + RRPV(2bit) + costClass(1bit)
_originKeys[i] : nuint Original pointer (reverse-deregisters Swiss Table entry on eviction)
_preprocess[i] : nint Writeback pre-processing function pointer (called before dirty eviction)
_writeback[i] : nint Writeback function pointer (called during dirty eviction)
_callbackCtx[i] : nuint Callback context (passed to preprocess/writeback)
```
**Eviction Flag Bit Layout** (`_evictionFlags[i]`):
The RRPV array uses byte storage for each cache line's usage state; the low 2 bits encode the RRPV value.
**RRPV Semantics**:
| RRPV | Meaning | Eviction Priority |
|------|---------------------------------|---------------------------|
| 0 | Will be re-accessed very soon | Lowest (do not evict) |
| 1 | Will be accessed shortly | Low |
| 2 | Will be accessed in a while | Medium |
| 3 | May never be accessed again | Highest (evict first) |
**RRPV Update Events**:
| Event | Operation | Description |
|--------------------|---------------------------------|------------------------------------------------------|
| Load (insert) | RRPV = 0 | Freshly loaded data will be used soon |
| Hit | `_evictionFlags[i] = 0` | Bits 1-2 cleared, RRPV reset to 0 |
| Eviction scan | Find lines with RRPV=3 to evict | If none found, increment all RRPV +1 (aging) |
**Advantages over the v0.1 four-level circular linked list**:
| Comparison | v0.1 Circular List | v0.3 RRPV Mask + Callbacks |
|----------------------|-------------------------------------|--------------------------------------|
| Data structure | 4 doubly-linked lists + next ptrs | Byte array + function pointer arrays |
| Warm-up operation | Unlink + relink (4 pointer writes) | `+= 1` (1-byte write) |
| Aging operation | Per-node list migration | SIMD batch +1 (16 entries/cycle) |
| Eviction scan | Iterate list head (pointer chasing) | SIMD pattern matching (16/cycle) |
| Cache friendliness | Poor (pointer chasing) | Good (contiguous memory scan) |
| Reference protection | None | Lines with refCount > 0 not evicted |
| Writeback support | None | Callback-function driven |
### 3.4 Writeback Callback Mechanism
**Responsibility**: Provides flexible writeback strategies for writable cache lines, using native function pointers to avoid managed overhead.
**Design Principle**: All writable cache lines are treated as dirty by default. On eviction or explicit flush,
L4 itself does not manage the writeback strategy; instead, the bound predictor is responsible for implementing it.
The predictor is defined via the following interface:
```
public interface IPrefetchPredict
{
int Predict(
Span<CacheLinePrediction> node
);
bool Prefetch(
nuint origin,
ulong policy,
nuint cache
);
void WriteBack(
nuint origin,
ulong policy,
nuint cache
);
}
```
**Typical Callback Implementations**:
```
Scenario 1: Segment accumulation (FMA partial sum)
preprocess = null (no pre-processing needed; data is already a double array)
writeback = AccumulateAdd (element-wise dst[i] += src[i], AVX2-vectorized)
Scenario 2: Writeback of packed intermediate results
preprocess = UnpackKernelRow (reverse-repack from Pack format to row-major order)
writeback = AccumulateAdd
Scenario 3: PLU decomposition intermediate data
preprocess = null
writeback = DirectCopy (direct overwrite memcpy)
```
### 3.5 CacheLine (Cache Line Handle)
**Responsibility**: A cache line access handle held by the compute thread; prevents in-use lines from being evicted via reference counting.
```csharp
public ref struct CacheLine : IDisposable
{
private ref int _activeCount; // L4 global active count
private ref int _refCount; // Per-line reference count
private nuint _handle; // Local cache data address
// Construction: Interlocked.Increment(ref _activeCount)
// Interlocked.Increment(ref _refCount)
// Dispose: Interlocked.Decrement on both counters
}
```
- `ref struct` ensures it cannot escape to the heap
- Reference counts use `Interlocked` atomic operations for thread safety
- Lines with `_refCount > 0` are skipped during eviction scans
- StreamTransparent mode returns a CacheLine that does not occupy a cache slot
- Both read-only and writable paths return the same CacheLine type,
callers decide read/write behavior via `.Address`
### 3.6 Predictor (Prefetch Predictor)
**Responsibility**: Provides deterministic prefetch predictions for L4 based on the windowed OPT strategy.
**Core Principle**: Matrix computation task sequences are deterministic — the scheduler pre-allocates
task blocks `(kr, kc, zz)` for each thread, allowing precise computation of the cache line keys
required at each future step. This gives L4 a predictive capability that hardware caches cannot
possess: **direct visibility into the future**, rather than inferring the future from history as
strategies like Hawkeye do.
```
Predictor Sliding Window:
Task sequence: ... [T_now] [T+1] [T+2] ... [T+1023] ... [T+2047]
↑ execution point ↑ buffer tail
Window capacity = 2048 prediction entries
Prefetch trigger threshold = trigger refill when prefetch frontier
leads execution point by fewer than 1024 steps
Refill batch size = 64 entries (avoids excessive single-refill latency)
```
**数据结构**:
```csharp
internal class Predictor
{
private const int WindowCapacity = 2048;
private const int RefillThreshold = 1024;
private PrefetchEntry[] _buffer; // Circular prediction buffer
private int _head; // Prefetch frontier (prefetched up to here)
private int _tail; // Production position (scheduler fills up to here)
private int _executed; // Execution point (compute thread has completed up to here)
private HashSet<nuint> _activeKeys; // All active keys within the window (deduplicated)
}
struct PrefetchEntry
{
public nuint Key; // Cache line identifier
public nuint SourceAddress; // Source data address
public bool NeedsRepack; // Whether non-contiguous repacking is needed
}
```
**Cooperation with Eviction Policy**:
```
Eviction priority (from highest to lowest priority):
1. refCount==0, not in window, RRPV==3, dirty==0 ← First (zero-cost eviction)
2. refCount==0, not in window, RRPV==3, dirty==1 ← Second (needs writeback but cold)
3. refCount==0, not in window, RRPV<3, dirty==0 ← Ordered by RRPV
4. refCount==0, not in window, RRPV<3, dirty==1 ← Ordered by RRPV
5. refCount==0, in window ← Avoid evicting if possible
6. refCount>0 ← Never evict
```
Windowed OPT means most eviction decisions do not depend on RRPV — "not in window" alone is
sufficient to determine evictability. RRPV provides fine-grained differentiation among
out-of-window lines; the dirty bit preferentially evicts clean lines at equal RRPV to avoid writeback overhead.
### 3.7 Concurrency Model
L4 uses a **no-extra-threads** design; all operations are performed by compute threads themselves:
| Operation | Executor | Concurrency |
|------------------------------------|---------------------------------|--------------------------------------|
| Lookup (Swiss Table read) | Any compute thread | ✅ Fully parallel, lock-free |
| HIT RRPV update | Any compute thread | ✅ Single-byte write, no sync needed |
| refCount increment/decrement | Any compute thread | ✅ Interlocked atomic operations |
| Prefetch refresh (eviction + load) | CAS-winner thread | Serial (only one thread enters) |
| Dirty writeback | CAS-winner thread | Serial (within eviction flow) |
| FlushAllDirty | Single thread after computation | Serial (no concurrency) |
**Prefetch Refresh CAS Lock**:
```csharp
private int _refreshLock; // 0=free, 1=locked
private void TryRefreshPrefetch()
{
if (Interlocked.CompareExchange(ref _refreshLock, 1, 0) != 0)
return; // Another thread is doing it; current thread continues computing
try { /* eviction + prefetch */ }
finally { Volatile.Write(ref _refreshLock, 0); }
}
```
**Swiss Table Concurrent Read Safety Guarantee**: The prefetch refresh thread follows strict write ordering
on insertion: write Slab data first, then write eviction metadata and callback pointers, `sfence`, then
write the Swiss Table control byte and key/value, ensuring that when a compute thread reads the control
byte, the data is already fully visible.
---
## 4. Operation Flows
### 4.1 Cache Lookup (Read-Only Hit)
1. Compute CRC32C hash of the original pointer
2. Swiss Table lookup: SIMD scan ctrl → compare key → return cache pointer
3. Update RRPV: `_evictionFlags[slot] &= 0xF9` (RRPV zeroed, dirty bit unchanged)
4. Construct CacheLine handle (refCount++)
5. Return CacheLine; upper-layer code accesses local data via `.Address`
### 4.2 Cache Lookup (Writable Hit)
1. Swiss Table lookup hits
2. Update RRPV: `_evictionFlags[slot] &= 0xF9`
3. Construct CacheLine handle (refCount++)
4. Return CacheLine; upper-layer code reads/writes the cache line via `.Address` (e.g., partial sum +=)
### 4.3 Cache Miss (Read-Only)
1. Swiss Table lookup misses
2. Compute thread enters `Thread.Sleep(0)` spin loop
3. Wait for Predictor-driven prefetch refresh to load the line into L4
4. Exit wait loop when Swiss Table shows a hit
5. Construct CacheLine handle and return
Under normal operation, the Predictor always leads the execution point by 1024+ steps,
misses occur only during cold start or extreme bursts.
### 4.4 Cache Miss (Writable)
1. Swiss Table lookup misses
2. Allocate a cache line from Slab
3. If Slab has no free slots → trigger eviction (see 4.6)
4. Zero-initialize the cache line (`Unsafe.InitBlock(dest, 0, 8192)`)
5. Register writeback callbacks: `_preprocess[slot]`, `_writeback[slot]`, `_callbackCtx[slot]`
6. Set eviction flags: `dirty=1, RRPV=0`
7. Write `_originKeys[slot]`
8. `sfence`
9. Write Swiss Table mapping
10. Construct CacheLine handle and return
### 4.5 Prefetch Refresh
Triggered by compute threads in `StepComplete()`; CAS lock ensures single-thread execution:
1. Synchronize refFlags snapshot (`_refCounts[i] > 0 → _refFlags[i] = 1`)
2. Retrieve pending prefetch entries from Predictor (up to 64 at a time)
3. For each entry:
- Swiss Table hit → mark RRPV=0 (hot-line protection)
- Swiss Table miss → execute load:
a. Allocate cache line from Slab
b. If Slab has no free slots → trigger eviction (see 4.6)
c. Execute Pack reorder or memcpy based on `NeedsRepack` flag
d. Write eviction metadata (originKey, RRPV=0, dirty=0, callbacks=null)
e. `sfence`
f. Write Swiss Table mapping
### 4.6 Cache Eviction (SIMD Scan + Writeback)
```
Eviction scan pseudocode:
Pass 1: Find refCount==0, RRPV==3, dirty==0 (zero-cost eviction)
for i in range(scanCursor, CacheLineCount), step=16:
refChunk = SSE2.Load(refFlags + i)
refFreeMask = SSE2.MoveMask(SSE2.CompareEqual(refChunk, zero))
evChunk = SSE2.Load(evictionFlags + i)
// RRPV==3 and dirty==0 → byte & 0x07 == 0x06
masked = SSE2.And(evChunk, 0x07)
coldCleanMask = SSE2.MoveMask(SSE2.CompareEqual(masked, 0x06))
candidates = refFreeMask & coldCleanMask
while candidates != 0:
bit = TrailingZeroCount(candidates)
idx = i + bit
if originKeys[idx] not in Predictor.ActiveKeys:
evict(idx) // No writeback, discard directly
return
candidates &= candidates - 1
Pass 2: Find refCount==0, RRPV==3, dirty==1 (needs writeback)
Same as above, but match byte & 0x07 == 0x07
evict triggers writeback callbacks
Pass 3: Increment all RRPV+1 (aging), retry
Up to 3 rounds
```
**逐出执行**:
```csharp
private void Evict(int slot)
{
byte flags = _evictionFlags[slot];
// Dirty writeback
if ((flags & 0x01) != 0)
{
nuint origin = _originKeys[slot];
nuint cached = SlotAddressFromIndex(slot);
nuint ctx = _callbackCtx[slot];
// 1. Pre-processing (optional)
var preprocess = (delegate* unmanaged<nuint, nuint, void>)_preprocess[slot];
if (preprocess != null)
preprocess(cached, ctx);
// 2. Writeback
var writeback = (delegate* unmanaged<nuint, nuint, nuint, void>)_writeback[slot];
if (writeback != null)
writeback(origin, cached, ctx);
}
// Standard eviction
_map.TryRemove(_originKeys[slot], out _);
_slab.Free(SlotAddressFromIndex(slot));
_evictionFlags[slot] = 0;
_originKeys[slot] = 0;
_preprocess[slot] = 0;
_writeback[slot] = 0;
_callbackCtx[slot] = 0;
}
```
### 4.7 显式刷新
```csharp
/// <summary>
/// Explicitly writes back the specified dirty cache line. Called after all segments complete.
/// </summary>
public void FlushDirty(nuint originKey)
{
if (_map.TryGet(originKey, out nuint addr))
{
int slot = SlotIndex(addr);
if ((_evictionFlags[slot] & 0x01) != 0)
{
// Execute pre-processing + writeback
InvokeWriteBack(slot);
_evictionFlags[slot] &= 0xFE; // Clear dirty bit
}
}
}
/// <summary>
/// Writes back all dirty cache lines in bulk. Called after entire matrix computation completes.
/// </summary>
public void FlushAllDirty()
{
for (int i = 0; i < CacheLineCount; i++)
{
if ((_evictionFlags[i] & 0x01) != 0 && _originKeys[i] != 0)
{
InvokeWriteBack(i);
_evictionFlags[i] &= 0xFE;
}
}
}
```
### 4.8 Working Mode Switch
```csharp
public enum L4Mode
{
/// <summary>
/// Pass-through mode: Map() returns the original pointer directly; L4 does not intervene.
/// Suitable for single-NUMA + no-repack scenarios (e.g., vector addition, scalar multiplication).
/// </summary>
StreamTransparent,
/// <summary>
/// Buffer mode: full lookup/Pack/cache/eviction pipeline.
/// Suitable for FMA scenarios requiring repacking or multi-NUMA scenarios.
/// </summary>
CacheBuffer
}
```
Mode is determined at construction time and does not change at runtime. Decision logic:
- Requires Pack reordering (FMA core rows of matrix A) → CacheBuffer
- Multiple NUMA nodes → CacheBuffer
- Requires segment accumulation → CacheBuffer
- Single NUMA + no reordering + no segment → StreamTransparent
---
## 5. Segment Accumulation Scenario
### 5.1 Problem
For matrix multiplication `C = A × B` when the K dimension is large, segmented accumulation is required:
```
segment 0: C_partial = A[kr, 0..KC] × B[0..KC, kc]
segment 1: C_partial += A[kr, KC..2KC] × B[KC..2KC, kc]
...
segment N: C_partial += A[kr, N*KC..K] × B[N*KC..K, kc]
Final: C[kr, kc] = Σ all segment partial sums
```
If each segment's result is `+=` directly to the C matrix,
and C resides on a remote NUMA node, every segment incurs cross-node write latency.
### 5.2 L4 Solution
Temporarily store C's partial sums in L4's writable cache lines:
```
segment 0: c = l4.MapWritable(C_key, null, &AccumulateAdd)
// First miss → allocate zero-initialized cache line
FMA(c.Address, a.Address, b.Address) // c += a*b
c.Dispose()
segment 1: c = l4.MapWritable(C_key, null, &AccumulateAdd)
// Hit! Returns existing partial sum cache line
FMA(c.Address, a.Address, b.Address) // c += a*b
c.Dispose()
... after all segments complete ...
l4.FlushDirty(C_key)
// Calls AccumulateAdd(C_origin, cacheline, ctx)
// → Adds local partial sum back to remote C matrix in one shot
```
### 5.3 Benefits
```
Without segment buffering (OpenBLAS approach):
Each segment writes C matrix across NUMA
Remote writes per output tile = Zk times
Per-write latency = ~200-300ns (cross-node)
With L4 segment buffering:
All segment += operations complete locally in L4 cache lines
Partial sums likely remain in L1/L2 (8KB << L1 capacity)
Remote writes per output tile = 1 (final FlushDirty)
Writeback uses AVX2-vectorized stores: 8192B = 32 × 256-bit stores
Reduction ratio = Zk : 1
```
---
## 6. Uniqueness and Advantages
### 6.1 Deterministic Prefetch (Windowed OPT)
Essential difference from hardware cache replacement policies (LRU, RRIP, Hawkeye):
| Policy | Information Source | Prediction Method | Accuracy |
|---------------------|-------------------------|-------------------------------|---------------------------|
| LRU | Past access order | Least recently used | Moderate |
| RRIP | Insert/hit markers | Predict reuse distance | Moderate-High |
| Hawkeye | OPTgen retrospection | Learn past to infer future | High |
| **L4 Predictor** | **Future task queue** | **Direct future visibility** | **Perfect** (within window) |
L4's Predictor is a **deterministic Oracle** — it does not predict; it knows.
Because matrix computation task sequences are fully determined at scheduler allocation time.
### 6.2 NUMA Adaptability
The same code automatically gains NUMA benefits on different hardware configurations:
| Hardware Configuration | L4 Behavior |
|-------------------------|-------------------------------------------------------------|
| Single-socket CPU | Pack cache + segment accumulation buffer |
| Dual-socket CPU | Pack cache + NUMA migration cache + segment accum. |
| Hybrid-architecture CPU | Pack cache + cross-domain migration cache + segment accum. |
### 6.3 Callback-Driven Writeback Strategy
Using native function pointers for writeback avoids:
- Virtual method call overhead
- Interface dispatch overhead
- Delegate GC pressure
- Hard-coding specific writeback logic
Callers register `preprocess` and `writeback` callbacks at `MapWritable` time,
L4 invokes them directly through function pointers on eviction or Flush,
**zero managed overhead, zero virtual dispatch, performance equivalent to C function calls**.
### 6.4 Reference Count Protection
Through `CacheLine` handle reference counting, cache lines actively in use by compute threads are
absolutely never evicted (including writeback not being triggered). The SIMD eviction scan skips
lines with `refCount > 0` immediately via the `_refFlags` array, without checking linked lists or acquiring locks.
### 6.5 Cache Line Size Aligned to Matrix Kernel
The 8192B cache line size is not arbitrary:
- 1 `double MAT_KERNEL` (4×4 double matrix) = 128 Bytes
- 1 cache line = 64 `MAT_KERNEL` units
- Matrix multiplication micro-kernels operate on data sizes perfectly aligned to cache lines
- Compute kernel functions operate on cache lines as units; no cross-cache-line accesses occur
### 6.6 Fixed-Capacity Determinism
- Slab allocator never expands → memory footprint upper bound is known (64 MB)
- SIMD eviction scan → latency upper bound is known (at most 3 aging rounds × 512 SIMD comparisons)
- Swiss Table fixed 16,384 slots → lookup latency upper bound is known
- Writeback callback latency is controlled by the caller → predictable
- Worst-case L4 latency = lookup + eviction (incl. writeback) + allocation + memcpy, all statically computable
### 6.7 Reuse of Existing StgSharp Infrastructure
| L4 Component | Reused StgSharp Infrastructure |
|-----------------------|-------------------------------------------------------|
| Mapping table | `SwissTable` (implemented and benchmark-validated) |
| Mapping table SIMD | `IBucketScanner` + `SIMDID` architecture detection |
| Cache line alloc | `SlabAllocator.FromBuffer<T>()` |
| Hash computation | Hardware `CRC32C` instruction |
| Memory allocation | `NativeMemory.AlignedAlloc` |
| Future multi-platform | `IBucketScanner` + NRGA compilation pipeline |
### 6.8 Originality Analysis
After surveying existing mainstream numerical computation libraries, L4's design has
**no direct precedent or equivalent in known public implementations**.
#### NUMA Strategy Comparison with Existing Libraries
| Library / Framework | NUMA Strategy | Software Cache Layer | Mapping Table | Eviction Policy | Dirty Writeback |
|---------------------|-------------------------------------|---------------------|-----------------|-------------------------|--------------------|
| Intel MKL | Thread pinning + `numactl` | None | None | None | None |
| OpenBLAS | Thread affinity + block alignment | None | None | None | None |
| BLIS | Multi-level block alignment L1/L2/L3| None | None | None | None |
| Eigen | No explicit NUMA handling | None | None | None | None |
| cuBLAS / rocBLAS | GPU memory (hardware-solved) | None | None | None | None |
| **StgSharp L4** | **Software-managed thread-local cache** | **Yes** | **Swiss Table** | **RRPV + windowed OPT** | **Callback-driven** |
---
## 7. 从 v0.1 到 v0.3 的变更摘要
| 项目 | v0.1 | v0.3 |
|-----------------|-------------------------------|---------------------------------------|
| 逐出数据结构 | 4 级环形双向链表 | RRPV 2-bit 掩码 + 8KB SoA 数组 |
| 逐出扫描方式 | 链表头指针遍历 | SSE2 批量模式匹配(16 行/周期) |
| 热���更新 | 链表节点摘插(4 指针写) | 单字节位运算 `&= 0x03` |
| 老化操作 | 逐节点移链表 | SIMD 批量 +1 |
| 引用保护 | 无 | refCount > 0 时 SIMD 扫描跳过 |
| 预取策略 | 无 | Predictor 窗口 OPT(2048 条缓冲) |
| 线程模型 | L4 服务线程 + Command Queue | 无额外线程,CAS 抢锁 |
| 命令队列 | 链式 CommandCache 分段 | 不需要(预取刷新替代命令驱动) |
| 缓存行元数据 | Slab 分配的 CacheLineHead 结构 | SoA 平行数组 + 函数指针数组 |
| 工作模式 | 单一模式 | StreamTransparent / CacheBuffer 切换 |
| 写回策略 | 无(只读缓存) | dirty 位 + preprocess/writeback 回调 |
| 写回机制 | 无 | 本机函数指针,零托管开销 |
---
## 8. 未来工作
- [ ] L4 根类型重构(组合 Swiss Table、Slab、SoA 元数据、Predictor)
- [ ] Predictor 与矩阵乘法调度器的集成
- [ ] Segment 累加写回的 AVX2 向量化实现
- [ ] SIMD 逐出扫描的 AVX2 升级(32 行/周期)
- [ ] Swiss Table ARM NEON 实现
- [ ] Swiss Table AVX-512 升级(64 槽扫描从 16 次降至 4 次)
- [ ] 墓碑累积监控与自动 rehash 策略
- [ ] 性能基准测试(NUMA 本地 vs 远端访问延迟对比)
- [ ] Segment 累加 vs 直接写回的 benchmark 对比
- [ ] 与逐线程矩阵上下文的集成验证
- [ ] 双路 6242R 环境下的 NUMA 搬运性能验证
- [ ] 远期:PPC64LE 平台的 VSX 原生 Swiss Table 扫描支持
---
## 9. 参考
- Google Abseil Swiss Tables Design Notes
- HP Labs RRIP (Re-Reference Interval Prediction) 论文
- Hawkeye: Efficient Cache Replacement via OPTgen (ISCA 2017)
- StgSharp HLSF Allocator 设计文档
- StgSharp SIMD Architecture (`SIMDID` 体系)
- StgSharp NRGA Roadmap
- StgSharp `SlabAllocator<T>` 实现
- StgSharp `SwissTable` 实现及 Benchmark
@@ -41,16 +41,16 @@ namespace StgSharp.HighPerformance.Memory
_Capacity = count;
_recycle = new((int)_Capacity);
_recycle.PushRange(init, (int)count);
_recycle.FillRange(init, buffer);
_currentIndex = _Capacity;
static void init(
Span<nuint> arr,
int _
nuint buffer
)
{
for (int i = 0; i < arr.Length; i++) {
arr[i] = (nuint)i;
arr[i] = buffer + ((nuint)i * (nuint)Unsafe.SizeOf<T>());
}
}
}
@@ -25,8 +25,6 @@ namespace StgSharp.Timing
private EventHandler? _refreshHandler, _missHandler;
private int _size, _id;
private long _pendingSpans; // Number of unconsumed span ticks since last Wait.
private readonly ManualResetEventSlim _event = new();
private readonly object _participantObject = new();
public event EventHandler MissTimeSpanRefreshed
@@ -57,15 +55,17 @@ namespace StgSharp.Timing
/// </summary>
public long PendingSpanCount => Volatile.Read(ref _pendingSpans);
internal ManualResetEventSlim AwaitingEvent => _event;
internal ManualResetEventSlim ResetEvent { get; } = new();
public void Dispose()
{
#pragma warning disable CA1816
// mark provider as ended so external users can observe terminal state
Volatile.Write(ref _endedFlag, 1);
s_unusedID.Push(TokenID);
_event.Set();
ResetEvent.Set();
GC.SuppressFinalize(this);
#pragma warning restore CA1816
}
public override int GetHashCode()
@@ -84,7 +84,7 @@ namespace StgSharp.Timing
public void WaitNextSpan()
{
_event.Wait();
ResetEvent.Wait();
// Consume one span slot (clamp at 0 so multiple subscribers won't make it negative).
while (true)
@@ -99,7 +99,7 @@ namespace StgSharp.Timing
if (cur - 1 == 0)
{
// Last consumer resets the gate for the next tick.
_event.Reset();
ResetEvent.Reset();
}
return;
}
@@ -108,7 +108,7 @@ namespace StgSharp.Timing
public async Task WaitNextSpanAsync()
{
await Task.Run(() => _event.Wait()).ConfigureAwait(false);
await Task.Run(ResetEvent.Wait).ConfigureAwait(false);
while (true)
{
long cur = Volatile.Read(ref _pendingSpans);
@@ -120,7 +120,7 @@ namespace StgSharp.Timing
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
_event.Reset();
ResetEvent.Reset();
}
break;
@@ -138,7 +138,7 @@ namespace StgSharp.Timing
)
{
// Block the current thread until the event is signaled or cancellation requested.
_event.Wait(ct);
ResetEvent.Wait(ct);
// consume one pending span (same logic as non-cancellable variant)
while (true)
@@ -152,7 +152,7 @@ namespace StgSharp.Timing
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
_event.Reset();
ResetEvent.Reset();
}
break;
@@ -203,7 +203,7 @@ namespace StgSharp.Timing
if (Interlocked.CompareExchange(ref _pendingSpans, toIssue, 0) == 0)
{
// Open the gate for all current waiters (up to `_size`).
_event.Set();
ResetEvent.Set();
_refreshHandler?.Invoke(this, EventArgs.Empty);
}
} else
@@ -47,7 +47,7 @@ namespace StgSharp.Timing
}
TokenID = id;
_size = maxWaitCount > 0 ? maxWaitCount : 1;
_event = new ManualResetEventSlim(initialState:false, spinCount:0);
ResetEvent = new ManualResetEventSlim(initialState:false, spinCount:0);
_pendingSpans = 0;
}
@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.Common.Timing
{
internal class TimeThreadProfile
{
}
}
@@ -20,8 +20,7 @@ namespace StgSharp.RegularAnalysis.Text
public RegexAstNode(
Token<RegexElementLabel> source
)
: this(RegexCommonPayload.FromToken(source))
{ }
: this(RegexCommonPayload.FromToken(source)) { }
public RegexAstNode(
RegexAstPayload payload
@@ -32,6 +31,8 @@ namespace StgSharp.RegularAnalysis.Text
public int EnumState { get; set; }
public ISyntaxPayload<RegexElementLabel> Source => Payload;
public long NodeFlag => (long)Label;
public static RegexAstNode Empty { get; }
@@ -69,6 +70,8 @@ namespace StgSharp.RegularAnalysis.Text
}
}
public RegexAstPayload Payload { get; private protected set; }
public RegexElementLabel EqualityTypeConvert => Label;
public RegexElementLabel Label => Payload.Flag;
@@ -80,13 +83,7 @@ namespace StgSharp.RegularAnalysis.Text
_ => Payload.Source
};
public RegexAstPayload Payload { get; private protected set; }
public ISyntaxPayload<RegexElementLabel> Source => Payload;
internal TPayload PayloadAs<TPayload>() where TPayload : RegexAstPayload =>
Payload as TPayload ?? throw new InvalidOperationException(
$"Node {Label} does not carry {typeof(TPayload).Name}.");
public string SourceCode => Source.Source;
public void AppendNode(
RegexAstNode nextToken
@@ -106,7 +103,7 @@ namespace StgSharp.RegularAnalysis.Text
right &= RegexElementLabel.OPERATOR;
int v_left = (int)left, v_right = (int)right;
if (v_left == 0 || v_right == 0) {
return (false,0);
return (false, 0);
}
return (true,v_left - v_right);
}
@@ -148,6 +145,14 @@ namespace StgSharp.RegularAnalysis.Text
return new EmptyRegexAstNode();
}
internal TPayload PayloadAs<TPayload>() where TPayload : RegexAstPayload
{
return
Payload as TPayload ??
throw new InvalidOperationException(
$"Node {Label} does not carry {typeof(TPayload).Name}.");
}
internal void RemoveAndAsChild(
bool useLeft
)
@@ -24,9 +24,9 @@ namespace StgSharp.RegularAnalysis.Text
category:"NGRA.TextRegex",
isEnabledByDefault:true);
private static readonly DiagnosticDescriptor CompileFailDiag = new(id:"NGRA005",
title:"Unexpected exception when compiling",
messageFormat:"Unexpected exception handles when compiling expression \"{0}\":{1}",
private static readonly DiagnosticDescriptor CompileFailDiag = new(id:"NGRA006",
title:"Regular expression compilation failed",
messageFormat:"Failed to compile expression \"{0}\": {1}",
defaultSeverity:DiagnosticSeverity.Error,
category:"NGRA.TextRegex",
isEnabledByDefault:true);
@@ -39,8 +39,8 @@ namespace StgSharp.RegularAnalysis.Text
isEnabledByDefault:true);
private static readonly DiagnosticDescriptor IncorrectReturnTypeDiag = new(id:"NGRA003",
title:"Text regex binding must be partial",
messageFormat:"Method {0} must be partial to receive generated code",
title:$"Returning type must be {typeof(TextRegex).FullName}",
messageFormat:$"Method {{0}} must return {typeof(TextRegex).FullName}",
defaultSeverity:DiagnosticSeverity.Error,
category:"NGRA.TextRegex",
isEnabledByDefault:true);
@@ -53,8 +53,8 @@ namespace StgSharp.RegularAnalysis.Text
isEnabledByDefault:true);
private static readonly DiagnosticDescriptor NotPartialDiag = new(id:"NGRA001",
title:$"Returning type must be {typeof(TextRegex).FullName}",
messageFormat:$"Method {{0}} must return {typeof(TextRegex).FullName}",
title:"Text regex binding must be partial",
messageFormat:"Method {0} must be partial to receive generated code",
defaultSeverity:DiagnosticSeverity.Error,
category:"NGRA.TextRegex",
isEnabledByDefault:true);
@@ -24,6 +24,7 @@ namespace StgSharp.RegularAnalysis.Text
RegexAstNode ast,
in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context,
bool is_find = false
)
@@ -38,16 +39,16 @@ namespace StgSharp.RegularAnalysis.Text
switch (op)
{
case RegexElementLabel.CONCAT:
GenerateConcat(root, sc, func_list);
GenerateConcat(root, sc, func_list, context);
break;
case RegexElementLabel.COUNT:
GenerateCount(root, sc, func_list);
GenerateCount(root, sc, func_list, context);
break;
case RegexElementLabel.GROUP_BEGIN:
GenerateGroup(root, sc, func_list);
GenerateGroup(root, sc, func_list, context);
break;
case RegexElementLabel.ALT:
GenerateAlt(root, sc, func_list);
GenerateAlt(root, sc, func_list, context);
break;
}
} else
@@ -67,6 +68,20 @@ namespace StgSharp.RegularAnalysis.Text
#region supporting method
private static string FormatStringLiteral(
string value
)
{
return SyntaxFactory.Literal(value).Text;
}
private static string FormatCharLiteral(
char value
)
{
return SyntaxFactory.Literal(value).Text;
}
private static bool IsNamedFind(
RegexAstNode node
)
@@ -99,32 +114,48 @@ namespace StgSharp.RegularAnalysis.Text
private static void GenerateAlt(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
)
{
RegexAstNode left = node.Left;
RegexAstNode right = node.Right;
RegexAstNode current = left;
RegexAstNode current = node.Right;
string exit_label = context.RequestExitAlt();
while (RegexAstNode.IsNullOrEmpty(current))
{
GenerateMethodSource(current, sc, func_list);
current = current.Next;
}
current = right;
while (RegexAstNode.IsNullOrEmpty(current))
{
GenerateMethodSource(current, sc, func_list);
current = current.Next;
RegexElementLabel op = current.Label & RegexElementLabel.SEQUENCE;
if (op != 0)
{
string code = current.SourceCode;
switch (op)
{
case RegexElementLabel.UNIT:
_ = sc.AppendLine(@$"if({context.RemainSpan}[0] == {FormatCharLiteral(code[0])})")
.AppendLine("{")
.AppendLine($@"goto {exit_label};")
.AppendLine("}");
break;
case RegexElementLabel.UNIT_SET:
IReadOnlyList<RegexCharSet> split = current.PayloadAs<RegexCharSetPayload>()
.Set;
RegexCharSet first_set = split[0];
for (int i = 1; i < split.Count - 1; i++) { }
RegexCharSet last_set = split[^1];
break;
}
} else
{
GenerateMethodSource(current, sc, func_list, context);
current = current.Next;
}
}
_ = sc.AppendLine($@"{exit_label}: {{ }}");
}
private static void GenerateGroup(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
)
{
RegexAstNode left = node.Left;
@@ -134,10 +165,10 @@ namespace StgSharp.RegularAnalysis.Text
if (left is not null)
{
GenerateMethodSource(left, sc, func_list);
GenerateMethodSource(left, sc, func_list, context);
} else
if (right is not null) {
GenerateMethodSource(right, sc, func_list);
GenerateMethodSource(right, sc, func_list, context);
}
string group_name = config.Name;
@@ -147,7 +178,8 @@ namespace StgSharp.RegularAnalysis.Text
private static void GenerateConcat(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
)
{
RegexAstNode left = node.Left;
@@ -162,40 +194,41 @@ namespace StgSharp.RegularAnalysis.Text
right is not null)
{
// TODO use find
GenerateMethodSource(right, sc, func_list, true);
GenerateMethodSource(right, sc, func_list, context, true);
} else if (IsNamedFind(node))
{
if (right is not null)
{
// TODO named find
GenerateMethodSource(right, sc, func_list, true);
GenerateMethodSource(right, sc, func_list, context, true);
} else
{
// TODO pack rest of string to group
}
} else
{
GenerateMethodSource(left, sc, func_list);
GenerateMethodSource(left, sc, func_list, context);
if (right is not null) {
GenerateMethodSource(right, sc, func_list);
GenerateMethodSource(right, sc, func_list, context);
}
}
}
if (right is not null) {
GenerateMethodSource(right, sc, func_list);
GenerateMethodSource(right, sc, func_list, context);
}
}
private static void GenerateCount(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
)
{
RegexAstNode value = node.Left ?? node.Right;
if ((value.Label & RegexElementLabel.SEQUENCE) == 0)
{
GenerateMethodSource(value, sc, func_list);
GenerateMethodSource(value, sc, func_list, context);
} else
{
RegexCountPayload payload = value.PayloadAs<RegexCountPayload>();
@@ -6,9 +6,11 @@
// -----------------------------------------------------------------------------
using Microsoft.CodeAnalysis.Text;
using StgSharp.PipeLine;
using StgSharp.RegularAnalysis.Abstraction;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -27,206 +29,33 @@ namespace StgSharp.RegularAnalysis.Text
/// a block scoped scratch variable. Scratch names are never reused, so a nested block can
/// always declare its own without colliding with an enclosing one.
/// </remarks>
private sealed class SourceGenContext
internal sealed class SourceGenContext
{
/// <summary>
/// Name of the match method parameter holding the input sequence.
/// </summary>
public const string _inputSpan = "text";
public const string __input_span = "input";
/// <summary>
/// Result of the instruction emitted immediately before the current one.
/// </summary>
public const string _stepResult = "__ok";
public const string _remain_span = "__remain";
private readonly Dictionary<string, int> _groupIndex = [];
private readonly HashSet<int> _observedLines = [];
private int _exit_alt_count;
private int _anonymousGroup;
public string RemainSpan { get; } = _remain_span;
private int _count_idx = 0;
private int _scratchIndex;
private readonly SortedSet<int> _lineSlots = [];
private readonly SortedSet<int> _regionSlots = [];
public string FirstOfRemain { get; } = $@"{_remain_span}[0]";
public string RemainingSpan { get; } = "__remaining_span";
public string InputString { get; } = __input_span;
public string InputSpan { get; } = _inputSpan;
public string StepResult { get; } = _stepResult;
// region id -> entry count from first to last occurrence, filled once per pattern
public int[] RegionExtent { get; set; } = [];
public string[] BeginRegion()
public SequenceEmitter<string> DeclareGlobalFields()
{
return [
"do",
"{"
];
SequenceEmitter<string> sc = new();
return sc.AppendLine($@"{ROS_char} {RemainSpan};");
}
public string[] CloseRegion()
public string RequestExitAlt()
{
return [
"} while(true);"
];
}
/// <summary>
/// Emits declarations for every slot requested during body generation.
/// </summary>
/// <remarks>
/// Must be called after the method body has been generated, but the produced lines
/// have to be placed before it.
/// </remarks>
public SequenceEmitter<string> EmitDeclarationsTo(
SequenceEmitter<string> emitter
)
{
if (emitter is null) {
return null!;
}
_ = emitter.AppendLine($@"{ROS_char} {RemainingSpan} = {_inputSpan};")
.AppendLine($@"bool {_stepResult} = true;");
foreach (int region in _regionSlots) {
_ = emitter.AppendLine($@"int {RegionAnchor(region)} = 0;")
.AppendLine($@"bool {RegionResult(region)} = false;");
}
foreach (int line in _lineSlots) {
_ = emitter.AppendLine($@"bool {LineResult(line)} = false;");
}
foreach (int group in _groupIndex.Values) {
_ = emitter.AppendLine($@"int __g{group}_begin = -1;")
.AppendLine($@"int __g{group}_end = -1;");
}
return emitter;
}
public string GetVarGroupBegin(
string name
)
{
return $"__g{ResolveGroup(name)}_begin";
}
public string GetVarGroupEnd(
string name
)
{
return $"__g{ResolveGroup(name)}_end";
}
/// <summary>
/// Indicates whether the instruction at line <paramref name="line" /> has to persist
/// its result into a line slot.
/// </summary>
public bool IsLineObserved(
int line
)
{
return _observedLines.Contains(line);
}
/// <summary>
/// Name of the slot holding the match result of the instruction at line <paramref
/// name="line" />. Only lines actually referenced by an <see
/// cref="RegexIRPrefixMask.Observe_Line" /> prefix get a slot.
/// </summary>
public string LineResult(
int line
)
{
_ = _lineSlots.Add(line);
return $"__l{line}_ok";
}
public string MakeRemainOffset(
string offset_name
)
{
return @$"{RemainingSpan} = {RemainingSpan}[{offset_name}..];";
}
public string MakeRemainOffset(
int offset
)
{
return @$"{RemainingSpan} = {RemainingSpan}[{offset}..];";
}
/// <summary>
/// Records that some prefix observes the result of the instruction at line <paramref
/// name="line" />.
/// </summary>
public void MarkLineObserved(
int line
)
{
_ = _observedLines.Add(line);
}
/// <summary>
/// Allocates a fresh block scoped scratch variable name.
/// </summary>
/// <param name="tag">
/// Short role marker, for example <c> n </c> for a counter or <c> ch </c> for a
/// character.
/// </param>
public string NewScratch(
string tag
)
{
return $"__{tag}{_scratchIndex++}";
}
/// <summary>
/// Name of the slot holding the cursor value captured when region <paramref
/// name="region" /> was entered.
/// </summary>
public string RegionAnchor(
int region
)
{
_ = _regionSlots.Add(region);
return $"__r{region}_begin";
}
/// <summary>
/// Name of the slot holding the match result of region <paramref name="region" />.
/// </summary>
public string RegionResult(
int region
)
{
_ = _regionSlots.Add(region);
return $"__r{region}_ok";
}
public string RequireNewCounter()
{
string counter_name = $"__count{_count_idx}";
_count_idx++;
return counter_name;
}
private int ResolveGroup(
string name
)
{
string key = string.IsNullOrEmpty(name) ? $"#{_anonymousGroup++}" : name;
if (_groupIndex.TryGetValue(key, out int index)) {
return index;
}
index = _groupIndex.Count;
_groupIndex[key] = index;
return index;
string code = $@"_exit_alt_{_exit_alt_count}";
_exit_alt_count++;
return code;
}
}
@@ -43,6 +43,8 @@ namespace StgSharp.RegularAnalysis.Text
SequenceEmitter<string> ce = new();
SourceRecord gen_source = binding.Source;
TypeSource type = binding.Source.ContainingType;
SequenceEmitter<string> method_body = binding.GeneratedSource;
SourceGenContext regex_gen_context = binding.SourceContext;
_ = file_name_sb.Append(CultureInfo.InvariantCulture, $"{gen_source.NamespaceName}.");
_ = ce.AppendLine($@"// <auto-generated/>")
@@ -75,15 +77,12 @@ namespace StgSharp.RegularAnalysis.Text
} while (type is not null);
static_modifier = gen_source.IsMethodStatic ? "static" : string.Empty;
_ = ce.AppendLine(@$"{ToKeyword(gen_source.MethodAccessibility)} {static_modifier} {Text_Regex} {gen_source.MethodName}()")
_ = ce.AppendLine(@$"{ToKeyword(gen_source.MethodAccessibility)} {static_modifier} {Text_Regex} {gen_source.MethodName}({ROS_char} {regex_gen_context.InputString})")
.AppendLine("{")
/**/ //TODO return single instance of the TextRegex object
.Append(method_body)
.AppendLine("}");
// TODO inject method source code here
for (int i = 0; i < brace_count; i++)
{
for (int i = 0; i < brace_count; i++) {
_ = ce.AppendLine("}");
}
@@ -104,10 +103,6 @@ namespace StgSharp.RegularAnalysis.Text
_ = file_name_sb.Append(CultureInfo.InvariantCulture, $@"{binding.Source.MethodName}.g.cs");
string file_name = file_name_sb.ToString();
context.AddSource(file_name, code_source.ToString());
foreach (Diagnostic diag in binding.RegexDiagnostic) {
context.ReportDiagnostic(diag);
}
}
private static string ToKeyword(
@@ -76,12 +76,17 @@ namespace StgSharp.RegularAnalysis.Text
private TextRegexBinding(
List<Diagnostic> diagnostics,
SequenceEmitter<string> generatedSource,
SourceRecord source
SourceRecord source,
SourceGenContext sourceContext,
TextRegexSource? analyzedSource
)
{
_diagnostics = diagnostics ?? [];
GeneratedSource = generatedSource ?? new();
Source = source;
AnalyzedSource = analyzedSource;
IsValid = !_diagnostics.Exists(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
SourceContext = sourceContext;
}
public TextRegexBinding()
@@ -89,11 +94,15 @@ namespace StgSharp.RegularAnalysis.Text
_diagnostics = [];
}
public TextRegexSource? AnalyzedSource { get; set; }
public bool IsValid { get; private set; } = true;
public IReadOnlyList<Diagnostic> RegexDiagnostic => _diagnostics;
public SequenceEmitter<string> GeneratedSource { get; set; }
public SequenceEmitter<string> GeneratedSource { get; set; } = new();
public SourceGenContext SourceContext { get; set; } = null!;
public SourceRecord Source { get; set; }
@@ -121,7 +130,8 @@ namespace StgSharp.RegularAnalysis.Text
SequenceEmitter<string> code = GeneratedSource.Clone();
List<Diagnostic> diag = new(_diagnostics.Count);
diag.AddRange(this._diagnostics);
TextRegexBinding binding = new(diag, code, Source with { });
TextRegexBinding binding = new(diag, code, Source with { }, SourceContext,
AnalyzedSource);
return binding;
}
@@ -9,14 +9,8 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using StgSharp.RegularAnalysis.Abstraction;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.RegularAnalysis.Text
{
@@ -30,21 +24,54 @@ namespace StgSharp.RegularAnalysis.Text
IncrementalGeneratorInitializationContext context
)
{
IncrementalValuesProvider<TextRegexBinding> provider = context.SyntaxProvider
.ForAttributeWithMetadataName<TextRegexBinding>(
IncrementalValuesProvider<TextRegexBinding> candidates =
//
context.SyntaxProvider
.ForAttributeWithMetadataName<TextRegexBinding>(
TextRegexTypeName,
static(
_,
_
) => true, BindTextRegexCandidate);
IncrementalValuesProvider<TextRegexBinding> match = RegisterDiagnosticBypass(context, provider);
IncrementalValuesProvider<TextRegexBinding> validCandidates =
RegisterDiagnosticBypass(context, candidates);
IncrementalValuesProvider<TextRegexBinding> compile = match.Select(CompileRegexMethodSource);
IncrementalValuesProvider<TextRegexBinding> analyzed =
validCandidates.Select(AnalyzeRegexMethod);
IncrementalValuesProvider<TextRegexBinding> compile_success = RegisterDiagnosticBypass(context, compile);
IncrementalValuesProvider<TextRegexBinding> validAnalyzed =
RegisterDiagnosticBypass(context, analyzed);
context.RegisterSourceOutput(compile_success, GenerateRegexSourceFile);
IncrementalValuesProvider<TextRegexBinding> generated =
validAnalyzed.Select(CompileRegexMethodSource);
IncrementalValuesProvider<TextRegexBinding> validGenerated =
RegisterDiagnosticBypass(context, generated);
context.RegisterSourceOutput(validGenerated, GenerateRegexSourceFile);
}
private static TextRegexBinding AnalyzeRegexMethod(
TextRegexBinding binding,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
if (!binding.IsValid) {
return binding;
}
TextRegexBinding result = binding.Clone();
string pattern = result.Source.Pattern;
TextRegexSource analyzedSource = RegexAnalyzer.Analyze(pattern);
result.AnalyzedSource = analyzedSource;
if (analyzedSource.AnalyseException is not null) {
result.AddDiagnostic(CompileFailDiag, result.Source.PatternDefineLocation, pattern,
analyzedSource.AnalyseException.Message);
}
return result;
}
private static TextRegexBinding BindTextRegexCandidate(
@@ -71,7 +98,7 @@ namespace StgSharp.RegularAnalysis.Text
TextRegexBinding bind = new();
if (!method_decl.Modifiers.Any(SyntaxKind.PartialKeyword)) {
bind.AddDiagnostic(NotPartialDiag, method_decl.GetLocation(), method_decl.GetText());
bind.AddDiagnostic(NotPartialDiag, method_decl.GetLocation(), method_symbol.Name);
}
if (method_decl.ParameterList.Parameters.Count != 0) {
bind.AddDiagnostic(NotParameterLessDiag, method_decl.GetLocation(), method_decl.GetText());
@@ -97,9 +124,9 @@ namespace StgSharp.RegularAnalysis.Text
while (contain is not null)
{
if (contain.IsRecord) {
bind.AddDiagnostic(CanNotBeRecordDiag, method_decl.GetLocation(), method_symbol.Name);
bind.AddDiagnostic(CanNotBeRecordDiag, method_decl.GetLocation(), contain.Name);
}
if (contain.TypeKind is not TypeKind.Class or TypeKind.Struct) {
if (contain.TypeKind is not (TypeKind.Class or TypeKind.Struct)) {
bind.AddDiagnostic(IncorrectContainingTypeDiag, method_decl.GetLocation(),
method_symbol.Name, contain.TypeKind);
}
@@ -133,23 +160,28 @@ namespace StgSharp.RegularAnalysis.Text
}
private static TextRegexBinding CompileRegexMethodSource(
TextRegexBinding source,
CancellationToken ct
TextRegexBinding binding,
CancellationToken cancellationToken
)
{
ct.ThrowIfCancellationRequested();
TextRegexBinding result = source.Clone();
string pattern = result.Source.Pattern;
TextRegexSource analyze_result = RegexAnalyzer.Analyze(pattern);
if (analyze_result.AnalyseException is not null)
{
result.AddDiagnostic(CompileFailDiag, null, analyze_result.AnalyseException.Message);
} else
{
SequenceEmitter<string> method_source = /**/ new()/*GenerateSource(analyze_result)/**/;
result.GeneratedSource = method_source;
cancellationToken.ThrowIfCancellationRequested();
TextRegexBinding result = binding.Clone();
RegexAstNode root = result.AnalyzedSource!.Ast.Root;
SourceGenContext generationContext = new();
SequenceEmitter<string> methodSource = new();
List<SequenceEmitter<string>> localFunctions = [];
GenerateMethodSource(root, methodSource, localFunctions, generationContext);
foreach (SequenceEmitter<string> localFunction in localFunctions) {
_ = methodSource.AppendLine()
.AppendLine()
.AppendLine("// local method for matching")
.Append(localFunction);
}
result.SourceContext = generationContext;
result.GeneratedSource = methodSource;
return result;
}
@@ -158,19 +190,18 @@ namespace StgSharp.RegularAnalysis.Text
IncrementalValuesProvider<TextRegexBinding> bindings
)
{
IncrementalValuesProvider<Diagnostic> error_path = bindings.Where(static binding => !binding.IsValid)
.SelectMany(static(
binding,
_
) => binding.RegexDiagnostic);
IncrementalValuesProvider<TextRegexBinding> correct_path = bindings.Where(static binding => binding.IsValid);
IncrementalValuesProvider<Diagnostic> errorPath =
bindings.Where(static binding => !binding.IsValid)
.SelectMany(static(
binding,
_
) => binding.RegexDiagnostic);
context.RegisterSourceOutput(errorPath, static(
productionContext,
diagnostic
) => productionContext.ReportDiagnostic(diagnostic));
context.RegisterSourceOutput(error_path, (
c,
diag
) => c.ReportDiagnostic(diag));
return correct_path;
return bindings.Where(static binding => binding.IsValid);
}
}