---
title: "多 Subagent 并发的 CPU 与内存基线"
description: "在零延迟假模型、debug 二进制、无 TUI 的合成负载下对比单 Agent 与 4 个并发子会话：4 倍工作量对应 3.56 倍 CPU（跨度 2.31–4.41），未观察到超线性放大；可测的成本驱动是单个会话的历史长度，不是并发数。会话持久化开关没有测出差异。"
---

> Documentation Index
> Fetch the complete documentation index at: https://konghayao.github.io/peri-cool/llms.txt
> Use this file to discover all available pages before exploring further.

# 多 Subagent 并发的 CPU 与内存基线

> 这是一次受控对比，被测对象是调试版二进制在一个合成负载下的 CPU 与内存。它能回答「并发本身有没有额外惩罚」和「成本随什么上升」，不能回答「真实使用下能省多少」——真实模型、release 构建与 TUI 渲染都不在本次测量范围内。

## 结论

1. 4 倍工作量对应 **3.56 倍 CPU**（中位数之比，跨度 2.31 到 4.41）。跨度覆盖 4.0，所以**不能断言低于线性**，只能说没有观察到超线性放大。
2. 4 个子会话**确实并行**：以每个子会话的首次请求到末次请求作为窗口，四段窗口的并集长度只占四段长度之和的 0.26 到 0.27，完全重合时该比值是 0.25。
3. 可测的成本驱动是**单个会话的历史长度**：每会话 20 轮时每轮 0.0166 CPU 秒，每会话 80 轮时升到 0.0222 到 0.0250 CPU 秒（1.34 倍与 1.51 倍）；同期最大请求体从 147 KB 涨到 415 到 439 KB。
4. **会话持久化不是这份负载里可测的 CPU 来源**：持久化开关的差值都小于组内离散；栈采样里也没有 SQLite 符号进入栈顶热点。
5. 峰值内存跟随**请求体规模**，不跟随子会话个数：4 个子会话、每会话 20 轮的峰值内存（105.5 MiB）低于单会话 80 轮（157.1 MiB）。
6. 「把 80 轮拆成 4 个 20 轮子会话更省 CPU」（比值 0.67）**不能读成「并发降低 CPU」**：这个对比同时把每会话历史从 80 轮缩到 20 轮，两个变量没有分离。

## 数据底稿与口径

1. **测量日期**：2026-10-16。
2. **被测二进制**：`target/debug/peri`，debug 构建、保留符号。测量时仓库 HEAD 为 `fdd90b7cc`，工作区仍有未提交改动，因此**二进制与 HEAD 不保证完全一致**。所有对比用的是同一个二进制，所以组间可比；绝对值不代表 release 构建。
3. **模型**：本地假模型服务（Anthropic SSE 格式），每个响应固定 4096 字节文本、固定 10 毫秒延迟。零网络、零推理等待。
4. **启动方式**：`peri -p` 非交互单轮问答模式，参数为 `--permission-mode bypass`、`--max-turns 400`、`--bare`。`--bare` 跳过了 hooks、插件与 MCP 服务初始化；`-p` 模式没有 TUI 渲染。
5. **CPU 口径**：`ps -o time=` 每 100 毫秒采样，取**进程累计 CPU TIME 的差分**，不使用瞬时 `%cpu`。
6. **环境**：Apple M5 Pro、18 核、macOS 26.5.1。机器**并非空闲**，运行期间 load average 在 11 到 14 之间，这是单 Agent 组离散偏大的可能原因。
7. **样本量**：主表每个配置 3 次运行；关闭持久化的探针各 2 次。
8. **一份批次被单独统计**：与 `sample` 采样同时进行的批次不进主表，因为 `sample` 本身会抢占 CPU 且对短运行影响明显（单 Agent 组带采样时 CPU 落在 2.13 到 3.19 秒，不带采样是 1.98 到 2.93 秒）。

## 负载定义

一个「脚本轮」包含三件事：

1. 一次假模型响应，固定 4096 字节文本。
2. 一次 `Bash(true)` 工具调用。
3. 一次会话持久化写入。

三种组织方式：

| 组 | 结构 | 脚本轮总数 |
| --- | --- | ---: |
| 单 Agent | 1 个会话连续跑 80 轮 | 80 |
| 4 子 Agent × 80 轮 | 一条助手消息发出 4 个 `Agent` 调用，每个子会话各 80 轮 | 320 |
| 4 子 Agent × 20 轮 | 同上，每个子会话各 20 轮 | 80 |

1. 前两组回答「每会话工作量相同、并发数从 1 涨到 4」的代价。
2. 第三组与「单 Agent」的总轮数相同，用于等总工作量对比；但它同时缩短了每会话历史长度。

并发有代码依据：同一条助手消息内的多个工具调用由 `join_all` 并发派发（`peri-agent/src/agent/stages/tool_dispatch/execution.rs:571`）。

## 结果

| 配置 | 运行次数 | CPU 秒 中位[范围] | CPU 秒/轮 中位[范围] | 墙钟 中位[范围] | 峰值内存 中位 | 最大请求体 |
| --- | ---: | --- | --- | --- | ---: | ---: |
| 单 Agent × 80 轮 | 3 | 2.00 [1.98, 2.93] | 0.0250 [0.0248, 0.0366] | 3.43s [3.40, 7.28] | 157.1 MiB | 439 KB |
| 4 子 Agent × 每会话 80 轮 | 3 | 7.12 [6.76, 8.74] | 0.0222 [0.0211, 0.0273] | 5.11s [4.92, 6.08] | 160.2 MiB | 415 KB |
| 4 子 Agent × 每会话 20 轮 | 3 | 1.33 [1.31, 1.47] | 0.0166 [0.0164, 0.0184] | 1.20s [1.19, 1.29] | 105.5 MiB | 147 KB |
| 单 Agent × 80 轮，关闭持久化 | 2 | 2.09 [2.06, 2.12] | 0.0262 [0.0258, 0.0265] | 3.53s [3.51, 3.55] | 159.1 MiB | 439 KB |
| 4 子 Agent × 每会话 80 轮，关闭持久化 | 2 | 7.05 [7.02, 7.08] | 0.0220 [0.0219, 0.0221] | 5.12s [5.09, 5.16] | 158.2 MiB | 415 KB |

1. **CPU 秒/轮** = 进程累计 CPU TIME 增量 ÷ 该组脚本轮总数。注意 4 子 Agent × 80 轮这一组的总数是 320，不是 80。
2. 峰值内存取该组各次运行峰值的中位数，按 1 MiB = 1024 KiB 换算，原始采样单位是 KB。
3. 最大请求体按十进制 KB 给出：439 KB 对应 439,057 字节，415 KB 对应 415,211 字节，147 KB 对应 147,191 字节。

## 比值与解读

1. **并发数 1 到 4，每会话工作量相同**：7.12 ÷ 2.00 = **3.56**。改用范围端点计算，跨度是 6.76 ÷ 2.93 = 2.31 到 8.74 ÷ 1.98 = 4.41。
   - 3.56 低于 4.0，但跨度包含 4.0，所以只能说「没有观察到超线性放大」，不能说「并发摊销了成本」。
2. **等总轮数（80 轮）的两种组织方式**：1.33 ÷ 2.00 = **0.67**，跨度 0.45 到 0.74。
   - 这个比值看起来是「拆成子会话更便宜」，但它同时把每会话历史从 80 轮压到 20 轮，最大请求体从 439 KB 降到 147 KB。**并发数与历史长度两个变量没有分离**，因此不能读成并发的收益。
3. **持久化开与关**：单 Agent 2.00 到 2.09 秒，4 子 Agent 7.12 到 7.05 秒。两个差值都小于各自组内离散，本负载下**没有可测差异**。
4. **每轮成本随每会话历史长度上升**：0.0166 → 0.0222（1.34 倍）与 0.0166 → 0.0250（1.51 倍）。同期最大请求体 147 KB → 415 到 439 KB。

## 栈采样的定性归属

1. 用 macOS `sample` 每 1 毫秒采样，只作**定性**归属，不作占比结论。
2. **口径警告**：`sample` 会采样进程内每个线程，且「Sort by top of stack」段只列出样本数不少于 5 的符号。这些计数是**墙钟采样份额**，不是 CPU 时间占比，也不能相加当作互斥占比。
3. 单 Agent 组那个文件里，栈顶符号共 38 项、合计 26,312 个样本，其中内核等待与空闲类 25,724 个，占 97.8%；非等待样本只有 588 个。
4. 4 子会话组那个文件里，栈顶符号共 67 项、合计 25,051 个样本，其中内核等待与空闲类 23,936 个，占 95.6%。
5. 非等待热点按样本数排序依次是：`_platform_memmove` 内存搬运、切片边界与指针对齐检查、`serde_json::format_escaped_str_contents`（模型请求的字符串序列化）、内存分配器 `_xzm_*`。
6. `__fork` 出现在栈顶 95 次，对应拉起 `Bash` 工具子进程。
7. **SQLite 没有进入栈顶热点**：两个文件里样本数不少于 5 的符号列表都没有 SQLite 或 sqlx 符号。SQLite 帧确实出现在更深的调用栈里，但那些节点的样本计数都是个位数量级，所以这条只能算弱证据，它与持久化探针「没有可测差异」的方向一致。
8. **线程数**：单 Agent 组进程内有 14 到 15 个线程，4 子会话组是 19 到 20 个。子会话确实带来了额外的执行线程。

## 与 2026-10-07 现场调查的关系

1. 更早的现场调查是 `spec/issues/2026-10-07-multi-subagent-cpu.md`，它自述为**非受控采样**，观察到一个 `target/debug/peri` 进程在多 Subagent 工具执行期间累计 CPU TIME 从 `7:05.97` 涨到 `7:11.34`。
2. 那次观察里的「CPU 168%」是**进程级 CPU**，多核时可以超过 100%，不是整机占用。
3. **那份调查的机制段落已经过时，不能读作当前版本的机制。** 它把成本指向 `write_work` 与 `encode<WorkState>`、`decode<WorkState>`；这条路径已被完整剥离：
   - 仓库根 `CLAUDE.md` 写明「不持久化 Agent 执行恢复状态」；
   - 剥离计划记录删除四张 `session_work_*` 与两张 `session_control_*` 表，schema 向前升级到 18；
   - 本次核对，`peri-resources/src/sessions/sqlite_store/session_data/work.rs`、`peri-resources/src/sessions/work/effects.rs`、`peri-acp-types/src/session_resources/work/processing.rs` 三个路径在当前树里都已不存在。
4. 当前的逐条写入入口是 `INSERT INTO messages`（`peri-resources/src/sessions/sqlite_store/session_data/history.rs:34`）。
5. 那份调查的待办里，「同模型、上下文与工具负载下对比单 Agent 与多 Agent」在本次合成负载下完成了一部分；真实模型、release 与 TUI 三个维度仍未验收。

## 不能声称

1. 不能宣布「多 Subagent 高 CPU 已根治」，也不能给出任何**真实环境降幅**——本次负载是零延迟假模型，真实模型下的等待占比与请求体规模完全不同。
2. 不能把 3.56 说成「低于线性」或「并发有摊销」，理由见比值第 1 条。
3. 不能把 0.67 说成「并发降低 CPU」，理由见比值第 2 条。
4. 不能把 `sample` 的栈顶计数当作占比或 CPU 时间。
5. 不能把 debug 二进制、`--bare`、无 TUI 的数字当作 release 或 TUI 上的表现。
6. 不能引用瞬时 `ps %cpu` 作为占比证据——本次没有使用这个口径。

## 局限

1. 每组只有 2 到 3 次运行，样本量小，中位数与范围都很粗。
2. 机器不空闲（load 11 到 14），单 Agent 组有一次运行墙钟到 7.28 秒，是组内离散的主要来源。
3. 负载是合成的：假模型零延迟，工具只有 `Bash(true)`，缺少真实的工具多样性与真实推理等待。
4. 只测了并发数 1 和 4，没有扫 2 与 8，所以无法判断比值是否随并发数单调变化。
5. 二进制与仓库 HEAD 不保证一致，绝对值只在这一次测量内部有效。
6. 没有覆盖 TUI 渲染成本，而现场调查里 render loop 是出现过的栈顶符号之一。

## 还没测的维度

按优先级列出，全部沿用本次的夹具：

1. **release 构建复测**：`./scripts/cargo-rmcp-patched.sh build --locked --release -p peri-tui --bin peri`。要注意 `[profile.release]` 设了 `strip = "symbols"`，release 只能做 CPU TIME 与内存计量，栈分类仍得用 debug。
2. **TUI 渲染维度**：在 tmux 内用同一剧本启动 TUI，追加采集帧延迟与绘制线程的样本。
3. **并发数扫描**：1、2、4、8 个子会话，每会话轮数相同，看比值是否随并发数线性变化。
4. **真实模型与真实网络**：需要凭据与外部成本，建议只做两组各 3 次，采集「CPU TIME 增量/轮」而不是墙钟。
5. **大载荷**：把每轮文本提到 64 到 256 KB，测「每轮成本对会话历史长度」的斜率，直接检验比值第 4 条的机制。

## 复核方式

1. 本文所有数字都来自 `/tmp` 下的原始输出：汇总 JSON、CPU 采样、请求时间线与 `sample` 栈文件，未写入仓库，也未做估算补齐。
2. 关键比值可以用主表的秒数直接复算：7.12 ÷ 2.00、1.33 ÷ 2.00、0.0166 对 0.0222 与 0.0250。
3. 内存取各次运行的峰值中位数，单位是 MiB，主表脚注给出了换算方式与原始字节数。
4. 栈采样只报了文件内可直接数出的样本数与符号名，没有把它们折算成百分比结论。

继续阅读[沙盒内存占用研究](/peri-cool/posts/research/sandbox-memory-usage/)、[子代理调度阶段统计](/peri-cool/posts/research/subagent-dispatch-phases/)与[缓存命中与压缩的实测画像](/peri-cool/posts/research/cache-hit-rate-study/)。

Source: https://konghayao.github.io/peri-cool/posts/research/subagent-cpu-baseline/index.mdx
