docs(algo): 5.3 节补窗首下标的圆周图(三种写法对照)

- 新增配图 采样环缓冲_窗首下标.svg:左侧 40 格圆环走一遍 head = 5、n = 20
  的「往回退 19 步」(跨过 0 与 39),右侧把直接减 / 16 位回绕后再取模 /
  先加 DEPTH 三种写法的结果并列,只有第三种落在 25。
- 与 5.2 的环形布局图分工:那张讲「窗在哪几格」,这张讲「下标怎么算出来」,
  不重复同一张图的语义。
- 生成器 gen_sample_ring_start_svg.py 顺带把 OVERFLOW 明细改到末尾统一打印
  (原先只在开头打,折行类告警只报计数、看不出是哪一条)。
- 门禁:doc-links --sections-strict 通过;id-check 新增行无未登记编号;
  generated-fresh 22 个生成器 17 一致、0 新漂移;产物连跑两次 sha1 不变。
This commit is contained in:
2026-10-29 17:28:48 +08:00
parent 929cacbd72
commit e71d623e60
4 changed files with 396 additions and 0 deletions
@@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="600" viewBox="0 0 1060 600" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1060" height="600" fill="#ffffff"/>
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">窗首 = head − n:head 不够减会走到负数,所以先加一圈 DEPTH 再减</text>
<text x="46.0" y="68.0" font-size="13" fill="#64748b">例:head = 5、n = 20 —— 最新点在 4,沿环往回退 19 步落在 25,途中跨过 0 与 39。DEPTH = 40 时 20 点窗正好占半圈,所以这条弧看起来是个半圆。</text>
<text x="46.0" y="88.0" font-size="13" fill="#64748b">同一个例子三种写法,只有第三种同时做到「同余正确」与「不发生下溢」:</text>
<circle cx="296.0" cy="352.0" r="148" fill="none" stroke="#cbd5e1" stroke-width="1.2"/>
<circle cx="296.0" cy="204.0" r="6" fill="#2563eb"/>
<circle cx="319.2" cy="205.8" r="6" fill="#2563eb"/>
<circle cx="341.7" cy="211.2" r="6" fill="#2563eb"/>
<circle cx="363.2" cy="220.1" r="6" fill="#2563eb"/>
<circle cx="383.0" cy="232.3" r="6" fill="#2563eb"/>
<circle cx="400.7" cy="247.3" r="6" fill="#d97706"/>
<circle cx="415.7" cy="265.0" r="5" fill="#94a3b8"/>
<circle cx="427.9" cy="284.8" r="5" fill="#94a3b8"/>
<circle cx="436.8" cy="306.3" r="5" fill="#94a3b8"/>
<circle cx="442.2" cy="328.8" r="5" fill="#94a3b8"/>
<circle cx="444.0" cy="352.0" r="5" fill="#94a3b8"/>
<circle cx="442.2" cy="375.2" r="5" fill="#94a3b8"/>
<circle cx="436.8" cy="397.7" r="5" fill="#94a3b8"/>
<circle cx="427.9" cy="419.2" r="5" fill="#94a3b8"/>
<circle cx="415.7" cy="439.0" r="5" fill="#94a3b8"/>
<circle cx="400.7" cy="456.7" r="5" fill="#94a3b8"/>
<circle cx="383.0" cy="471.7" r="5" fill="#94a3b8"/>
<circle cx="363.2" cy="483.9" r="5" fill="#94a3b8"/>
<circle cx="341.7" cy="492.8" r="5" fill="#94a3b8"/>
<circle cx="319.2" cy="498.2" r="5" fill="#94a3b8"/>
<circle cx="296.0" cy="500.0" r="5" fill="#94a3b8"/>
<circle cx="272.8" cy="498.2" r="5" fill="#94a3b8"/>
<circle cx="250.3" cy="492.8" r="5" fill="#94a3b8"/>
<circle cx="228.8" cy="483.9" r="5" fill="#94a3b8"/>
<circle cx="209.0" cy="471.7" r="5" fill="#94a3b8"/>
<circle cx="191.3" cy="456.7" r="6" fill="#2563eb"/>
<circle cx="176.3" cy="439.0" r="6" fill="#2563eb"/>
<circle cx="164.1" cy="419.2" r="6" fill="#2563eb"/>
<circle cx="155.2" cy="397.7" r="6" fill="#2563eb"/>
<circle cx="149.8" cy="375.2" r="6" fill="#2563eb"/>
<circle cx="148.0" cy="352.0" r="6" fill="#2563eb"/>
<circle cx="149.8" cy="328.8" r="6" fill="#2563eb"/>
<circle cx="155.2" cy="306.3" r="6" fill="#2563eb"/>
<circle cx="164.1" cy="284.8" r="6" fill="#2563eb"/>
<circle cx="176.3" cy="265.0" r="6" fill="#2563eb"/>
<circle cx="191.3" cy="247.3" r="6" fill="#2563eb"/>
<circle cx="209.0" cy="232.3" r="6" fill="#2563eb"/>
<circle cx="228.8" cy="220.1" r="6" fill="#2563eb"/>
<circle cx="250.3" cy="211.2" r="6" fill="#2563eb"/>
<circle cx="272.8" cy="205.8" r="6" fill="#2563eb"/>
<path d="M 372.4 246.8 A 130.0 130.0 0 0 0 204.1 443.9" fill="none" stroke="#e11d48" stroke-width="1.8"/><polygon points="204.1,443.9 193.8,440.0 200.3,433.6" fill="#e11d48"/>
<text x="226.5" y="316.6" font-size="11" text-anchor="middle" font-weight="700" fill="#e11d48">往回退 19 步</text>
<text x="296.0" y="186.0" font-size="12" text-anchor="middle" fill="#64748b">0</text>
<text x="466.0" y="356.0" font-size="12" text-anchor="start" fill="#64748b">10</text>
<text x="296.0" y="526.0" font-size="12" text-anchor="middle" fill="#64748b">20</text>
<text x="126.0" y="356.0" font-size="12" text-anchor="end" fill="#64748b">30</text>
<text x="419.0" y="229.0" font-size="11" text-anchor="start" font-weight="700" fill="#d97706">head = 5</text>
<text x="173.0" y="475.0" font-size="11" text-anchor="end" font-weight="700" fill="#2563eb">start = 25</text>
<text x="296.0" y="346.0" font-size="11" text-anchor="middle" fill="#94a3b8">环 = 40 格</text>
<text x="296.0" y="366.0" font-size="11" text-anchor="middle" fill="#94a3b8">窗 = 20 格(半圈)</text>
<rect x="596.0" y="186.0" width="418.0" height="96.0" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-width="1.8"/>
<text x="610.0" y="212.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">✗ 5 - 20</text>
<text x="1000.0" y="212.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">= -15</text>
<text x="610.0" y="234.0" font-size="11.5" fill="#64748b">负数。C99 的 % 向零截断,(-15) % 40 仍是 -15;</text>
<text x="610.0" y="250.0" font-size="11.5" fill="#64748b">转成无符号就是 65521,拿去索引 s_ring 即越界读。</text>
<rect x="596.0" y="296.0" width="418.0" height="96.0" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-width="1.8"/>
<text x="610.0" y="322.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">✗ (uint16_t)(5 - 20) % 40</text>
<text x="1000.0" y="322.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">= 65521 % 40 = 1</text>
<text x="610.0" y="344.0" font-size="11.5" fill="#64748b">65536 不是 40 的倍数:(x mod 65536) mod 40 ≠ x mod 40。</text>
<text x="610.0" y="360.0" font-size="11.5" fill="#64748b">回绕后再取模不等价,读到的是环尾附近而不是窗首。</text>
<rect x="596.0" y="406.0" width="418.0" height="96.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.8"/>
<text x="610.0" y="432.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#16a34a">✓ (5 + 40 - 20) % 40</text>
<text x="1000.0" y="432.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#16a34a">= 25 % 40 = 25</text>
<text x="610.0" y="454.0" font-size="11.5" fill="#64748b">就是窗首。加 40 不改同余类(40 ≡ 0 mod 40),</text>
<text x="610.0" y="470.0" font-size="11.5" fill="#64748b">中间值恒在 [0, 2·DEPTH - 2],两种提升规则都不下溢。</text>
<line x1="46.0" y1="530.0" x2="1014.0" y2="530.0" stroke="#cbd5e1" stroke-width="1"/>
<text x="46.0" y="556.0" font-size="13" fill="#64748b">加 DEPTH 不是「凑数」:它是环的周长,加一整圈等价于原地不动——但把减法从「可能为负」搬到了「必然非负」,取模才恢复成数学上的同余解。</text>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -197,6 +197,11 @@ flowchart TD
最后写入的一点是 `s_head - 1`,于是"以最新点为终点的 n 点窗"= 下标 `[s_head-n, s_head-1]`,
窗首(最老点)就是 `s_head - n`,落到 `[0, DEPTH - 1]` 要取模。
取一个"head 不够减"的例子走一遍:`head = 5`、`n = 20`——最新点在 4,沿环往回退 19 步落在 25,
途中跨过 0 与 39。直接写 `5 - 20` 会得到 −15,压根不在环上。
![head 不够减时直接减会走到负数:沿环往回退 19 步跨过 0,加一圈 DEPTH 才回到 25](../images/采样环缓冲_窗首下标.svg)
代码里写成 `(s_head + SAMPLE_RING_DEPTH - n) % SAMPLE_RING_DEPTH` 而不是 `(s_head - n) % DEPTH`,
是因为后者有两个独立的坑: