Files
HomeAgent/internal/plugin/proc/shm_profile_test.go
JianFeeeee 4340232bb0 test(proc): 共享内存数据面成本分解基准(纯测量,判断 C 化是否值得)
针对「共享内存应由 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 全绿,无回归。
2026-09-26 11:22:55 +08:00

271 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}