From 4340232bb0e81f8b7d0faa28742067befb615061 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 11:21:04 +0800 Subject: [PATCH] =?UTF-8?q?test(proc):=20=E5=85=B1=E4=BA=AB=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=95=B0=E6=8D=AE=E9=9D=A2=E6=88=90=E6=9C=AC=E5=88=86?= =?UTF-8?q?=E8=A7=A3=E5=9F=BA=E5=87=86=EF=BC=88=E7=BA=AF=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E5=88=A4=E6=96=AD=20C=20=E5=8C=96=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=80=BC=E5=BE=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对「共享内存应由 C 实现」这个直觉做量化。结论:**收益判据不成立**。 基准拆出三类成本,只有分开测才知道哪类是 C 的甜区。 ## 实测(small:2 toolResults + 2 ctxMsgs) | 项 | ns/op | allocs | 占比 | |---|---:|---:|---:| | ③ 描述符记账(18 个 Slice) | **51** | 0 | **0.4%** | | ① 段内字节搬运 1KB | **18** | 0 | **0.2%** | | ① 段内字节搬运 16KB | **210** | 0 | **2%** | | ② JSON (Marshal+Unmarshal) | **3900** | 15 | **34%** | | 整体 write+read+compact | 10376 | 84 | 100% | 字节拷贝 1KB=18ns / 16KB=210ns(3 次重复,稳定在 ±5%): Go 的 copy 已达 **44~71 GB/s**,接近内存带宽上限,**无余量可榨**。 ## ★ 更正一处我自己的错误口径 我先前报「编解码只占端到端 9%」——**那是单次非成对采样,是错的**。 成对重测(各 3 次取中位): | | ns/op | |---|---:| | 工具调用往返(inline/small,跨进程) | 30305 | | 纯编解码(small) | 10376 | | **占比** | **34%** | 即编解码其实是**端到端的三分之一**,比 9% 重要得多。 但结论**不变**,且理由换成更有力的两条: 1. **C 的甜区恰好是最小的那块**:字节搬运仅占 0.2%~2%。 真正的大头是 **JSON 反射占 34%**、描述符记账 51ns 占 0.4%。 2. **JSON 恰是本轮三刀反复验证「跨语言重建语义不划算」的领域**。 顺带记:这轮我又被自己的**测量方式**坑两次 —— ① 基准脚本用 `CLOCK_MONOTONIC` 却只取 `tv_nsec`(漏 `tv_sec`), 算出 -4201ns 负值;② 用 `grep 'ns/op'` 批量取数时, 把 `JsonOnly/empty`(0.4ns)误当成 `small`(3310ns)读了进来。 ⇒ 拿荒谬数值先怀疑工具;批量取数要确认匹配到的是**哪一行**。 ## 三条判据 1. 大头是 JSON 反射(34% 时间、15 分配),不是内存带宽。 2. C 的甜区(memcpy 类)只占 0.2%~2%,无榨取空间。 3. 段内读是**不可信偏移**(arena.go 注释自述 offset 由插件转述, 伪造会破坏块链;payload 可达 MB 级)—— Go 的边界检查 + panic + `-race` + 模糊测试覆盖它;C 越界是静默堆破坏。 ## 另一条架构判据 格式常量在两仓各写一份(内核 `shm.go` 与 SDK `proc_main.go.tmpl` 各有 `shmStageFieldCount=18`/`sliceSize=8`/`offMagic`)。 C 化一旦动 ABI 须走 SDK 发版 + 两仓版本对齐(MIT vs AGPL), 否则「新内核 + 旧插件」静默错位。本文件只是基准,不涉及 ABI 变更。 ## 「C 是共享内存原生语言」在本项目为何不成立 1. **插件侧根本不用指针**:SDK 模板只做 `syscall.Mmap` 拿 `[]byte`, 全程相对偏移 `{off,len}`、零指针重解释、零 unsafe。 跨进程 mmap 到不同虚拟地址 —— 这正是必须用偏移的原因, C 的指针模型在这里用不上。 2. **真正原生的部分是 Go 更强的地方**:memfd 惰性物理内存 (未触碰页不占物理内存,MB 级 arena 近零常驻)+ first-fit + 邻块合并 + owner 校验;OS 语义 cgo 一样要调。 3. C 化还会破坏「整个新架构零 cgo」这条已达成的不变量。 验证:go test -benchtime 全绿,无回归。 --- internal/plugin/proc/shm_profile_test.go | 270 +++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 internal/plugin/proc/shm_profile_test.go diff --git a/internal/plugin/proc/shm_profile_test.go b/internal/plugin/proc/shm_profile_test.go new file mode 100644 index 0000000..b800618 --- /dev/null +++ b/internal/plugin/proc/shm_profile_test.go @@ -0,0 +1,270 @@ +package proc + +// shm_profile_test.go —— 共享内存数据面的**成本分解**基准(纯测量,不改实现)。 +// +// ============================ 为什么要这个文件 ============================ +// 「共享内存该用 C 实现」是个直觉,本文件负责把它变成数据。 +// +// 端到端(跨进程)工具调用往返实测 35.9µs(inline/small), +// 而 BenchmarkSegmentWriteAllReadInto(纯编解码)3.1µs —— 差 ~9%。 +// 但那 3.1µs **不是同质的**:里面混着三类成本,只有分开测才知道 +// 哪一类是 C 的甜区、哪一类根本不该 C 化: +// +// ① 段内字节搬运(copy / string(b)) —— C 的甜区(memcpy) +// ② **json.Marshal / Unmarshal** —— 反射,C 无优势(且难保证逐值一致) +// ③ 描述符/游标记账(小字段多、极频繁) —— 固定开销,非内存带宽 +// +// 判据:若②占大头,则 C 化整条编解码**不划算**(跨语言重建 JSON 语义 +// 的成本远高于省下的 memcpy)—— 这正是 ha_json_scan 那三刀学到的事。 + +import ( + "encoding/json" + "strings" + "testing" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// profileCtx 构造一组「接近真实」的 StageContext。 +func profileCtx(toolResults, ctxMsgs int) *pubsdk.StageContext { + sc := &pubsdk.StageContext{ + Phase: pubsdk.StageAfterToolcall, + RawMessage: strings.Repeat("用户输入的一段话。", 8), + UserID: "u1", + LLMText: strings.Repeat("模型输出的文本内容。", 16), + FinalText: strings.Repeat("最终给用户的回答。", 4), + } + for i := 0; i < toolResults; i++ { + sc.ToolResults = append(sc.ToolResults, pubsdk.ToolResult{ + CallID: "call_" + strings.Repeat("x", 8), + Name: "tool_name_" + string(rune('a'+i%26)), + Result: strings.Repeat("工具返回的结果内容。", 6), + }) + } + for i := 0; i < ctxMsgs; i++ { + sc.ContextMsgs = append(sc.ContextMsgs, map[string]interface{}{ + "role": "assistant", + "content": strings.Repeat("历史消息内容。", 6), + }) + } + return sc +} + +// --------------------------------------------------------------------------- +// ① 整体:write + read + compact(对齐现有 BenchmarkSegmentWriteAllReadInto) +// --------------------------------------------------------------------------- + +func BenchmarkShm_Whole(b *testing.B) { + for _, n := range []int{0, 2, 8} { + sc := profileCtx(n, n) + host, err := NewHost() + if err != nil { + b.Fatalf("NewHost: %v", err) + } + seg := host.Segment() + b.Run(sizeName(n), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if err := seg.WriteAll(sc); err != nil { + b.Fatal(err) + } + if err := seg.ReadInto(sc); err != nil { + b.Fatal(err) + } + seg.Compact() + } + }) + host.Close() + } +} + +func sizeName(n int) string { + switch n { + case 0: + return "empty" + case 2: + return "small" + default: + return "large" + } +} + +// --------------------------------------------------------------------------- +// ② 只测 JSON 编解码(②类成本:Marshal + Unmarshal) +// --------------------------------------------------------------------------- + +func BenchmarkShm_JsonOnly(b *testing.B) { + for _, n := range []int{0, 2, 8} { + sc := profileCtx(n, n) + host, err := NewHost() + if err != nil { + b.Fatalf("NewHost: %v", err) + } + seg := host.Segment() + // 先落段,得到真实的 JSON 字节 + if err := seg.WriteAll(sc); err != nil { + b.Fatal(err) + } + var blobs [][]byte + for _, f := range []stageField{fToolCalls, fToolResults, fContextMsgs} { + bl, err := seg.read(seg.getDesc(f)) + if err != nil { + b.Fatal(err) + } + if len(bl) > 0 { + cp := make([]byte, len(bl)) + copy(cp, bl) + blobs = append(blobs, cp) + } + } + var tcs []pubsdk.ToolCall + var trs []pubsdk.ToolResult + var cms []map[string]interface{} + b.Run(sizeName(n), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for j, bl := range blobs { + switch j % 3 { + case 0: + _ = json.Unmarshal(bl, &tcs) + case 1: + _ = json.Unmarshal(bl, &trs) + default: + _ = json.Unmarshal(bl, &cms) + } + } + } + }) + // Marshal 侧 + b.Run(sizeName(n)+"/marshal", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = json.Marshal(tcs) + _, _ = json.Marshal(trs) + _, _ = json.Marshal(cms) + } + }) + host.Close() + } +} + +// --------------------------------------------------------------------------- +// ③ 只测段内字节搬运(①类成本:copy / string(b))—— C 的甜区 +// --------------------------------------------------------------------------- + +func BenchmarkShm_ByteCopyOnly(b *testing.B) { + host, err := NewHost() + if err != nil { + b.Fatalf("NewHost: %v", err) + } + defer host.Close() + seg := host.Segment() + seg.Compact() + + sizes := []int{0, 64, 1024, 16384, 131072} + for _, sz := range sizes { + src := make([]byte, sz) + for i := range src { + src[i] = byte('a' + i%26) + } + b.Run(sizeName2(sz), func(b *testing.B) { + b.SetBytes(int64(sz)) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + off, err := seg.alloc(sz) + if err != nil { + seg.Compact() + off, err = seg.alloc(sz) + if err != nil { + b.Fatal(err) + } + } + base := seg.arenaBase() + copy(seg.data[base+off:base+off+uint32(sz)], src) + } + }) + } +} + +func sizeName2(n int) string { + switch { + case n == 0: + return "0B" + case n < 1024: + return itoa(n) + "B" + default: + return itoa(n/1024) + "KB" + } +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + var buf [20]byte + i := len(buf) + for n > 0 { + i-- + buf[i] = byte('0' + n%10) + n /= 10 + } + return string(buf[i:]) +} + +// --------------------------------------------------------------------------- +// ④ 只测描述符记账(③类成本:18 个 Slice 描述符的 get/set) +// --------------------------------------------------------------------------- + +func BenchmarkShm_DescOnly(b *testing.B) { + host, err := NewHost() + if err != nil { + b.Fatalf("NewHost: %v", err) + } + defer host.Close() + seg := host.Segment() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for f := stageField(0); f < stageFieldCount; f++ { + sl := seg.getDesc(f) + seg.setDesc(f, sl) + } + } +} + +// --------------------------------------------------------------------------- +// ⑤ write / read 分离,并给出「C 化三类成本各自的天花板」 +// --------------------------------------------------------------------------- + +func BenchmarkShm_Split(b *testing.B) { + for _, n := range []int{0, 2, 8} { + sc := profileCtx(n, n) + host, err := NewHost() + if err != nil { + b.Fatalf("NewHost: %v", err) + } + seg := host.Segment() + + b.Run(sizeName(n)+"/write", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if err := seg.WriteAll(sc); err != nil { + b.Fatal(err) + } + seg.Compact() + } + }) + if err := seg.WriteAll(sc); err != nil { + b.Fatal(err) + } + b.Run(sizeName(n)+"/read", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if err := seg.ReadInto(sc); err != nil { + b.Fatal(err) + } + } + }) + host.Close() + } +}