L4 theoretical work
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# L4 运行模型
|
||||
|
||||
> 工作稿,2026-10-14。本文替代旧版 L4 设计草案,区分当前代码、已讨论方向和仍待确认的问题。
|
||||
|
||||
## 1. 范围与职责
|
||||
|
||||
每个 `L4` 实例绑定一个 NUMA 节点,拥有本地 arena,其中放置 TLSF 分配器状态和 L4 Swiss 映射表的双缓冲区。
|
||||
|
||||
- **L4** 负责本地分配、映射、缓冲区发布和阶段协调。
|
||||
- **Swiss** 保存并查询 L4 使用的映射/预测条目,不解释 source layout。
|
||||
- **Predictor** 解释自己的 source spec,生成 packed 数据,并负责格式相关的软件写回或撤销。
|
||||
- **PredictorMonitor** 计划负责预测器注册、稳定 ID、执行顺序和生命周期;它与 Swiss 中的预测任务/映射状态是两回事。
|
||||
|
||||
本文不假设自动写回。若 packed 数据需要写回,必须能识别其所属预测器并显式调用对应逻辑。
|
||||
|
||||
## 2. AddressSource 与缓存行数据
|
||||
|
||||
`AddressSource` 是 16 字节 key:
|
||||
|
||||
- `Address`:源地址或 source identity。
|
||||
- `Description`:64 位 source/layout spec,由所属预测器解释。
|
||||
|
||||
当前 Swiss 条目使用该 key 和一个 `ulong` value。源码中的 `CacheLineMetadata` 是 32 字节 TLSF metadata;`L4.Handle` 仍是 16 字节 ABI 占位。引用计数、驱逐保护和写回归属尚未端到端实现。
|
||||
|
||||
### 当前与候选 KVP 布局
|
||||
|
||||
- 设计讨论中的 `ushort` 是缓存行计数,不是预测器数量或预测器注册表容量。
|
||||
- 当前 Swiss bucket 将 key 与 `ulong` value 分开组织;现有的 8-KVP 交叉存储描述的是数据存储布局,不是 H1/hash 探测分组。
|
||||
- 一个候选布局是把 value 扩成 16 字节:保留现有 `ulong` cache-line metadata,再增加 8 字节存放预测计数等状态。与 16 字节 `AddressSource` 组成 32 字节 KVP,每条 64 字节 cache line 放两个 KVP。
|
||||
- 该方案旨在减少当前 8-KVP 交叉存储造成的 cache-line 分散/冲突。它只调整 KVP 的存储编排,不意味着 H1 探测组从当前 16 槽改成 8 槽。此方案尚未实现,需实测对查找和替换扫描的影响。
|
||||
|
||||
## 3. Swiss 映射
|
||||
|
||||
当前表有 1024 组,每组 16 个槽位。控制字节扫描使用 SSE2;x64 路径使用 CRC32C 混合两个 64 位 key 字段。两个表缓冲区都分配在 L4 arena 内。
|
||||
|
||||
published buffer 地址以 `ulong` 保存:
|
||||
|
||||
- `TryGet` 用一次 `Volatile.Read` 捕获 published pointer,并在整个查找中使用该快照。
|
||||
- `SwapBuffers` 用 `Interlocked.Exchange` 发布下一缓冲区,返回的旧 published pointer 成为下一轮 inactive write buffer。
|
||||
- 原子发布指针不代表旧 buffer 可以立即复用。Map/swap 阶段协议必须先确保读者不再访问旧 buffer,writer 才能清空或覆盖它。
|
||||
|
||||
当前只实现 x64/SSE4.2 后端,其他 ISA 后端尚未实现。
|
||||
|
||||
## 4. 预测器接口与阶段
|
||||
|
||||
当前接口骨架位于 `L4Predictor.cs`:
|
||||
|
||||
- `IL4Predictor` 是非泛型管理接口,暴露预测器 ID 和同步的 `PredictMission(in Span<AddressSource>)`。
|
||||
- `IL4Predictor<TMission>` 为预测器实现提供强类型 mission API。
|
||||
- `PredictMission` 填充调用方提供的 span,并返回有效预测数。span 可以由调用方 `stackalloc`,不得逃逸出本次同步调用。
|
||||
- `PredictorMonitor` 目前是空占位;预测器注册和生命周期尚未实现。
|
||||
|
||||
当前讨论中的 L4 生命周期分为不同阶段:
|
||||
|
||||
1. **注册预测器**:monitor 按稳定顺序保存异构预测器。固定容量数组(讨论过 256)是候选,其容量与缓存行计数无关。
|
||||
2. **Prefetch**:预测器按依赖顺序执行。每个 L4 同时只有一个活动 prefetch worker,但 prefetch 可与 Map 重叠,也可能跨 NUMA 节点访问目标数据。
|
||||
3. **发布**:完成后的 inactive Swiss buffer 被原子发布。Swiss 保存预测/映射任务状态;预测器注册表不是预测任务队列。
|
||||
4. **Map**:调用方查询已发布的 `AddressSource` 并取得 L4 handle。
|
||||
5. **撤销/写回**:所属预测器解释 packed 数据,并按需执行软件写回或撤销。
|
||||
|
||||
预测器接口本身是同步的。若预测结果需要在 `PredictMission` 返回后或另一线程继续使用,必须先复制到生命周期足够长的自有存储中。异步边界应位于拥有的 batch/work item 周围,而不能让 stack span 逃逸。
|
||||
|
||||
## 5. 预测窗口与 Map 同步
|
||||
|
||||
两个阈值含义不同:
|
||||
|
||||
- `_low_predict = 512`:预测触发/补充阈值。
|
||||
- `_predict_slow_path_count = 2 * LogicalCoreCount`:按 L4 绑定 NUMA 节点逻辑处理器数量计算的提前慢路径窗口。
|
||||
|
||||
当前 Map 的分流意图如下:
|
||||
|
||||
| 剩余预测数 | Map 行为 |
|
||||
|---|---|
|
||||
| 小于 512 | 尝试成为预测 writer;预测可能完成并发布新 buffer。竞争失败的调用走带读者计数的查找路径。 |
|
||||
| 512 到 `512 + _predict_slow_path_count` 之间 | 走带计数的慢查找路径,以便 swap 等待这些读者退出。 |
|
||||
| 大于或等于 `512 + _predict_slow_path_count` | 走无计数的直接查找快路径。 |
|
||||
|
||||
计数读者协议先等待 swap gate 打开,再增加 reader count 并复查 gate。如果 gate 在两步之间关闭,则撤销计数并重试。swap 方关闭 gate、等待已计数读者归零、交换 buffer、发布新的剩余预测数,最后重新打开 gate。
|
||||
|
||||
无计数快路径依赖预测提前量和阶段时序。这是性能启发式,本身不能证明一个被抢占的 reader 不会在旧 buffer 复用时仍继续访问。无计数 reader 与 buffer 生命周期之间的不变量,以及快慢路径边界,都仍需验证。
|
||||
|
||||
当前 `Predict()` 返回 `int`,但实现仍是返回 `0` 的空 stub,因此预测数生命周期尚未端到端运行。
|
||||
|
||||
## 6. 预测器注册、任务状态与在途缓冲
|
||||
|
||||
这三者职责不同,不应合并成一个队列:
|
||||
|
||||
- **预测器管理数组**:`PredictorMonitor` 的固定槽位数组,保存注册顺序、稳定 ID 和生命周期状态。容量上限(曾讨论 256)是预测器数量,与缓存行计数无关。预测器按依赖顺序依次执行,一个 L4 同一时刻只有一个 predictor worker 处于 prefetch 阶段,因此不需要多 lane 或 thread-local lane 归属。
|
||||
- **Swiss 预测任务状态**:保存/查找待映射的 `AddressSource` 条目;这是预测任务/映射状态,不是预测器注册表。
|
||||
- **在途预测器缓冲**:预测器已经产出预测,但对应实际工作尚未完成时,保留该预测器及其 mission/写回上下文。它与注册槽位不同:注册槽位描述“有哪些预测器”,在途缓冲描述“哪些预测工作还未完成”。具体容量、完成标记和回收时机尚待设计。
|
||||
|
||||
`PredictMission(in Span<AddressSource>)` 是同步接口,L4 可传入 stackalloc span。若返回后仍需跨线程或跨 NUMA 处理其内容,必须将内容复制到拥有明确生命周期的缓冲区;不得让 stack span 逃逸。由于预测器顺序有依赖,待处理预测必须按既定顺序交接。若采用异步 worker,队列应有界且不阻塞 Map 快路径。
|
||||
|
||||
## 7. 驱逐与写回策略
|
||||
|
||||
L4 不得推断或解码预测器的 packed 格式。若丢弃一个尚未写回、且对应实际工作仍在途的预测条目会丢失必要状态,就不能静默驱逐它。按缓存行的保护/引用字段或 sidecar 正在讨论中,引用/owner metadata 布局尚未定稿。
|
||||
|
||||
owner ID 可用于路由显式写回;但若多个预测器能独立声明同一条目,单一 owner 字段不够。引用计数可保护共享条目,而写回路径仍需获得预测器 identity/source spec。
|
||||
|
||||
## 8. 当前状态与待决问题
|
||||
|
||||
**源码中已实现:**
|
||||
|
||||
- NUMA 绑定的 L4 arena 和 TLSF 包装。
|
||||
- 双缓冲 Swiss 表及原子 published pointer 交换。
|
||||
- x64 CRC32C hash 和 SSE2 控制字节扫描。
|
||||
- 固定预测触发阈值,以及按 NUMA 节点逻辑处理器数量计算的提前慢路径窗口。
|
||||
- 慢路径的 Map reader gate/counter。
|
||||
- 同步的泛型/非泛型预测器接口骨架。
|
||||
|
||||
**尚未实现或尚未验证:**
|
||||
|
||||
- PredictorMonitor 注册表容量、注册/注销和生命周期状态转换。
|
||||
- 实际预测算法与有效预测条目的生成。
|
||||
- 端到端撤销/写回归属和缓存行驱逐保护。
|
||||
- Prefetch 与 Map 跨 NUMA 重叠时的自有 queue/batch 交接方式。
|
||||
- 无计数 Map 快路径不会与 published buffer 复用重叠的证明/测试。
|
||||
- 覆盖预测器发布、Map、撤销和 Dispose 的并发压力测试。
|
||||
|
||||
## 9. L4 的四个核心操作
|
||||
|
||||
### 预测(Prefetch)
|
||||
|
||||
Monitor 按注册顺序选择预测器,调用其同步预测接口;预测器解释自己的 source spec,将预测项写入调用方提供的 span。Swiss 管理预测/映射条目。若预测项对应的实际工作尚未完成,则放入在途预测器缓冲,保留后续完成、撤销或写回所需的归属信息。预测期间若没有可用的 cache-line 槽位,替换/保护策略由后续设计决定,不能隐式丢弃仍在途的条目。
|
||||
|
||||
### 映射(Map)
|
||||
|
||||
计算 worker 按预测器调度获取 `AddressSource`,通过 Swiss 查询 NUMA 本地的 packed 数据并得到 L4 handle。Map 不解释 `Description`,也不负责预测器注册或写回。
|
||||
|
||||
### 注册预测器
|
||||
|
||||
将预测器放入 `PredictorMonitor` 的管理槽位,分配稳定 ID 和执行顺序,并纳入生命周期监视。注册表槽位与 Swiss 预测条目、在途预测工作是不同资源。
|
||||
|
||||
### 移除预测器
|
||||
|
||||
先停止向该预测器分派新 mission,再等待其在途预测对应的实际工作完成或被撤销;需要的软件写回完成后,才能释放在途状态并回收注册槽位。具体等待与写回协议尚待设计。
|
||||
@@ -18,6 +18,7 @@ namespace Nitload.Common.HighPerformance.Memory
|
||||
{
|
||||
|
||||
private const int _low_predict = 512;
|
||||
private readonly int _predict_slow_path_count;
|
||||
|
||||
private int _predict_count;
|
||||
private int _swiss_reader_counter;
|
||||
@@ -29,7 +30,8 @@ namespace Nitload.Common.HighPerformance.Memory
|
||||
)
|
||||
{
|
||||
ulong handle;
|
||||
if (Volatile.Read(ref _predict_count) < _low_predict)
|
||||
int predictCount = Volatile.Read(ref _predict_count);
|
||||
if (predictCount < _low_predict)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _swiss_writer_lock, 1, 0) == 0)
|
||||
{
|
||||
@@ -57,6 +59,10 @@ namespace Nitload.Common.HighPerformance.Memory
|
||||
} else if (!TryGetAddress(source, out handle)) {
|
||||
throw new InvalidOperationException($"Unexpected map request: [address source: 0x{source.Address:x16}; layout profile: {source.Description:x16}].");
|
||||
}
|
||||
} else if (predictCount < _low_predict + _predict_slow_path_count)
|
||||
{
|
||||
if (!TryGetAddress(source, out handle))
|
||||
throw new InvalidOperationException($"Unexpected map request: [address source: 0x{source.Address:x16}; layout profile: {source.Description:x16}].");
|
||||
} else if (!_addressMap.TryGet(source, out handle)) {
|
||||
throw new InvalidOperationException($"Unexpected map request: [address source: 0x{source.Address:x16}; layout profile: {source.Description:x16}].");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Nitload.Common.HighPerformance.Memory
|
||||
{
|
||||
public class L4
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace Nitload.Common.HighPerformance.Memory
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(bufferSize, CacheLineMetadata.MaxBufferSize);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(maxCacheLineSize, CacheLineMetadata.MaxBufferSize);
|
||||
_bindedNuma = bindedNuma;
|
||||
_predict_slow_path_count = GetPredictSlowPathCount(bindedNuma);
|
||||
nuint normalizedBufferSize = (bufferSize + (PageSize - 1)) & (~(PageSize - 1));
|
||||
ByteCapacity = normalizedBufferSize;
|
||||
nuint swissBufferOffset = normalizedBufferSize + TLSF.RequestedAllocation;
|
||||
@@ -83,5 +84,16 @@ namespace Nitload.Common.HighPerformance.Memory
|
||||
|
||||
internal partial int Predict();
|
||||
|
||||
private static int GetPredictSlowPathCount(int bindedNuma)
|
||||
{
|
||||
foreach (NumaNode node in Numa.AllNumaNode)
|
||||
{
|
||||
if (node.NumaNodeId == (uint)bindedNuma)
|
||||
return checked(node.LogicalCoreCount * 2);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(bindedNuma), "No topology information exists for the bound NUMA node.");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// file="L4Predictor"
|
||||
// Project: StgSharp
|
||||
// Copyright (c) Nitload.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Nitload.Common.HighPerformance.Memory
|
||||
{
|
||||
public interface IL4Predictor
|
||||
{
|
||||
|
||||
int Id { get; set; }
|
||||
|
||||
int PredictMission(
|
||||
in Span<L4.AddressSource> RequiredBuffer
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public interface IL4Predictor<TMission> : IL4Predictor
|
||||
{
|
||||
|
||||
bool TryGetNextMission(
|
||||
out TMission mission
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
internal class PredictorMonitor { }
|
||||
}
|
||||
Reference in New Issue
Block a user