mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-26 12:23:23 +00:00
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 全绿,无回归。
This commit is contained in:
270
internal/plugin/proc/shm_profile_test.go
Normal file
270
internal/plugin/proc/shm_profile_test.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user