更正上一提交:争抢是 1.5x 不是 3.3x,并发现按币绑 worker 可省三分之一

上一提交的 3.3x 基线是废的:离线每次喂同一窗口,append_bar 一根没追
(实测 6 次调用 stream 2001→2001 零增长),测的只是信号链地板 30ms,
拿它比在场 inner_ms 又是一次口径不对齐——和刚撤回的 22/86 同一类错。

对齐后:在场每次平均追 2.81 根(2 worker 各存一份缓存、各自漏掉对方
处理过的根),离线同口径 67.3ms,在场 100ms → 争抢 1.49x。
顺带修正「重建占 21.5%」:真重建只有 0.31%,此前把换 worker 的小幅
回退误判成重建。

新杠杆:symbol 固定到同一 worker,每次只追 1 根,离线 67.3 → 45.0ms,
在场约省 33ms/币(inner_ms 三分之一)。心跳判定改指这一刀。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jack
2026-08-28 16:23:02 +08:00
co-authored by Cursor
parent cef894376c
commit af029bc26e
2 changed files with 45 additions and 16 deletions
+39 -10
View File
@@ -1683,25 +1683,54 @@ EMA/MACD/ATR 是递推的、BB/SMA/量比是窗口的,理论上都能 O(1) 更
出来的。换外生指标后相关降到 0.271。**凡是用被解释变量去构造解释变量的,先 出来的。换外生指标后相关降到 0.271。**凡是用被解释变量去构造解释变量的,先
怀疑循环。** 怀疑循环。**
注入合成负载复现(离线同一窗口2 核): 注入合成负载复现(离线,2 核):
| 背景 | `compute` 增量 | 放大 | | 背景 | `compute` | 放大 |
|---|---|---| |---|---|---|
| 空闲 | **29.9ms** | 1.00x | | 空闲 | 29.9ms | 1.00x |
| 1 个满载进程 | 41.7ms | 1.39x | | 1 个满载进程 | 41.7ms | 1.39x |
| 2 个满载进程 | 80.2ms | 2.68x | | 2 个满载进程 | 80.2ms | 2.68x |
| 3 个满载进程 | 85.4ms | 2.86x | | 3 个满载进程 | 85.4ms | 2.86x |
采集器实测:密集度=1 时 67ms(2.24x)、总体中位 100ms3.34x)。落在「1~2 个 ⚠️ **别拿这张表算争抢倍数——那个 29.9ms 基线是废的。** 它每次喂同一个窗口,
满载进程」到「2 个以上」之间,正是 **2 核上跑 主进程 + 2 worker** 该有的样子 `append_bar` 发现没有新根、一根没追(实测 6 次调用 stream 长度 2001→2001 零
(主进程常驻负载不小:10 路 K 线 WS + 10 路盘口 + 10Hz 采样)。 增长),测到的只是信号链本身。拿它去比在场的 `inner_ms` 得出「3.3 倍争抢」,
**又是一次口径不对齐——和被撤回的 22/86 同一类错,隔了一小时又犯一次。**
**结论与代价:** `inner_ms` 100ms = **约 30ms 真实计算 × 3.3 倍争抢** 口径对齐后:**在场每次调用平均追加 2.81 根**,因为 2 个 worker 各存一份缓存,
剩下的延迟不是算法性质的,继续压引擎收益有限(把 30ms 砍一半只换回 15ms×3.3 各自漏掉对方处理过的根(`stream_bars` 逐根 delta 在 ±1~±4 对称分布,正是两个
≈ 50ms,且越往下越难)。真正的杠杆是**核数**或**减币**。 独立计数器的指纹;**真重建只占 0.31%**,此前算的 21.5% 是把换 worker 的小幅
回退误判成重建)。而 `add_indicators` 每次追加都全表重算,所以追加成本随根数
近线性涨:
| 离线每次追加 | 1 根 | 2 根 | 3 根 | 4 根 |
|---|---|---|---|---|
| `compute` | **45.0ms** | 55.7ms | 70.1ms | 85.9ms |
在场平均 2.81 根 → 离线插值 **67.3ms**;在场 `inner_ms` 中位 **100ms**
**结论(已按口径修正):** `inner_ms` 100ms 拆成
| 档 | 耗时 | 性质 |
|---|---|---|
| 信号链 + `_rebuild` | ~30ms | 真实计算,追 0 根时的地板 |
| 追加 2.81 根(含 5m 腿) | ~37ms | **其中约 22ms 是缓存重复的浪费** |
| 2 核争抢 | ~33ms | **1.49x**,不是 3.3x |
⚠️ 争抢是 1.5 倍而非 3.3 倍,所以「加核」的收益比上一版写的小得多。
**新发现的便宜杠杆:按币绑定 worker。** 现在 symbol 落到哪个 worker 是随机的,
每个 worker 的缓存都只见到自己处理过的根,于是人人都要追 2.81 根。若把每个
symbol 固定到同一个 workersymbol hash → worker),每次只追 1 根:离线
67.3 → 45.0ms,在场约省 22ms × 1.49 ≈ **33ms/币,占 inner_ms 三分之一**
代价是失去负载均衡,但十币两 worker 本来就是静态五五分。
`add_indicators` 增量化(§5.71 末尾那条未做项)会同时压掉追加档的大头,
两件事叠加。
⚠️ 这和 §5.7 心跳里「币数 > 核数时加 worker 无用」不矛盾但要分清:**加 worker ⚠️ 这和 §5.7 心跳里「币数 > 核数时加 worker 无用」不矛盾但要分清:**加 worker
确实无用(worker 已等于核数)加核有用。** 两者在原判定里被混成一句了。 确实无用(worker 已等于核数)加核有用但只值 1.5 倍那部分。** 原判定把两者
混成一句了。
--- ---
+6 -6
View File
@@ -874,13 +874,13 @@ class Shadow:
if not INCR_ON: if not INCR_ON:
return (f"币数 {len(SYMS)} > 核数 {CORES},加 worker 无用(CPU 密集)" return (f"币数 {len(SYMS)} > 核数 {CORES},加 worker 无用(CPU 密集)"
f"。压单币耗时 → 开 SHADOW_INCR=1 走增量(实测 3.56x") f"。压单币耗时 → 开 SHADOW_INCR=1 走增量(实测 3.56x")
# 增量已生效时,单币耗时的大头不再是算法而是争抢:离线同一窗口 30ms, # 增量已生效时(§5.72 口径对齐后):单币 100ms ≈ 信号链 30ms + 追加
# 在场 100ms,注入合成负载能复现这个倍数(HANDOFF §5.72)。所以这里不能 # 2.81 根 37ms + 争抢 33ms。争抢只有 1.49x,加核收益有限;而追加那 37ms
# 再推荐「继续改算法」——那是按 3.3 倍虚高的数去优化 30ms 的真实计算 # 里约 22ms 纯属浪费——symbol 随机落 worker,各缓存都漏掉对方处理的根
return (f"币数 {len(SYMS)} > 核数 {CORES},增量已生效,加 worker 无用" return (f"币数 {len(SYMS)} > 核数 {CORES},增量已生效,加 worker 无用"
f"worker 已等于核数)。单币 {i:.0f}ms 里真实计算约 30ms" f"worker 已等于核数)。单币 {i:.0f}ms 里争抢只占约 1.5x"
f"其余是 {CORES} 核上争抢的放大 —— 杠杆是加核或减币" f"最便宜的一刀是按币绑定 worker(每次只追 1 根,省约三分之一)"
f"不是继续压算法") f"其次是 add_indicators 增量化")
async def run(self) -> None: async def run(self) -> None:
await self.start() await self.start()