mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 21:33:05 +00:00
perf(api): SSE 导航层返工 —— 5+ 次边界压成 1 次(三项赢,仍默认关闭)
按 sse-codec-c.md §6.4 的架构改造方向返工。**部分成功**:从「五项全输」
变成「三项赢 / 一项持平 / 一项输」,且所有场景分配数都下降。
## 改造内容
| 项 | 前 | 后 |
|---|---|---|
| cgo 边界次数 | 5+(每字段一次 findKey) | 1(ha_sse_chunk_locate) |
| 键查找 | 每键各扫一遍对象(6 趟) | 单趟分派(遍历成员表一次即分发) |
| 解码 | 每字段一次往返 + 各自 decBuf | 同一趟内写进一块 sbuf(1 次分配) |
| 成员表遍历 | 6 趟 | 2 趟(顶层 + delta) |
顺带修掉两处自造的浪费(都是「先扫一遍拿个数、再扫第二遍拿首元素」):
choices 数组的「数个数 + 取首元素」合一趟;choice0 内的 delta/finish_reason
合一趟。成员遍历实测 107ns/趟,省一趟就是省 107ns。
新增 ha_sse_chunk_locate:一次调用完成根校验 + 顶层分派 + choices[0] +
delta 分派 + content/reasoning/finish 解码,输出写调用方持有的 C 结构体
(C 结构体无 Go 指针 ⇒ 可安全传指针,消除 out-param 逃逸)。
choices_count>1 时直接回退(Go 侧 Unmarshal 会解析全部元素,本层只认 [0],
其余元素可能类型不符而让 Go 整块作废 ⇒ 无法保证等价)。
## 实测(50000 次 × 3 轮取中位)
| 场景 | Entry | GoOnly | 判定 |
|---|---|---|---|
| content_zh | 1540ns / 5allocs | 1871ns / 13allocs | 快 18%,分配 -62% |
| content_ascii | 1250ns / 5allocs | 1304ns / 13allocs | 持平,分配 -62% |
| finish | 820ns / 6allocs | 921ns / 12allocs | 快 11% |
| usage | 2530ns / 9allocs | 2591ns / 12allocs | 持平偏快 |
| toolcall | 3450ns / 20allocs | 3000ns / 21allocs | 慢 15% |
## toolcall 仍输的根因(已定位,非猜测)
分解测量:C 侧纯 C 零边界 = 766ns;Go 侧 []openAIToolCall unmarshal =
1305ns/15allocs;对照 Go 整块 unmarshal ≈ 2980ns。
问题在第二行:tool_calls 元素是对象,Arguments interface{} 需要真实的
map[string]interface{},必须走 encoding/json 的反射建树。
而为了定位已先做了一遍 C 扫描 ⇒ 同一份数据被解析了两次。
⇒ 不是 C 慢,是「扫两遍 vs 扫一遍」。
标量字段(content/reasoning/finish)C 能一次到位 ⇒ 那些场景赢;
需要建树的字段(tool_calls/usage)C 的定位是纯开销。
## 为什么仍默认关闭(理由充分,不是保守)
1. toolcall 是真实负载最常见的一类块(任何一次工具调用流),仍慢 15%
2. 18% 收益不足以抵消「与 encoding/json 语义并存的第二实现」的风险
3. 本刀原始动机在 toolcall 场景没有兑现:分配数 20 vs 21 几乎没降
⇒ 前提是先做「按字段类型决定是否 C 化」,让 toolcall 也不输,再重测。
## 正确性
6 万+ 差分用例(协议形态/真实负载/随机 JSON 3 万/随机字节 3 万)全过。
基准测量也修了:先前 C 基准脚本用 CLOCK_MONOTONIC 却只取 tv_nsec,
算出 -4201ns 的负值 —— 测量工具本身出错会直接毁掉结论。
## 验证
ASan+UBSan PASS;gcc+clang 零告警;arm64 交叉 0 告警;
libFuzzer 66 万次零崩溃;全量 go test 38 包 ok / 0 FAIL
This commit is contained in:
@ -278,3 +278,396 @@ int ha_sse_arg_string(const ha_span *val, char *out, size_t cap, size_t *outlen)
|
||||
*outlen = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ==================================================================== */
|
||||
/* 批量定位:一次调用返回全部字段 */
|
||||
/* ==================================================================== */
|
||||
|
||||
static int scan_first_and_count(const ha_span *arr, ha_span *first, int *count);
|
||||
static int chunk_choice_dispatch(ha_span choice, ha_chunk_out *out);
|
||||
static int chunk_delta_dispatch(ha_span delta, ha_chunk_out *out, int *dup);
|
||||
|
||||
static void slot_reset(ha_chunk_slot *s) {
|
||||
s->span.p = NULL;
|
||||
s->span.len = 0;
|
||||
s->kind = HA_CHUNK_KIND_ABSENT;
|
||||
}
|
||||
|
||||
static void chunk_out_reset(ha_chunk_out *o) {
|
||||
size_t i;
|
||||
for (i = 0; i < (size_t)HA_CHUNK_SLOT_COUNT; i++) {
|
||||
slot_reset(&o->slot[i]);
|
||||
}
|
||||
o->content_off = 0; o->content_len = 0;
|
||||
o->reasoning_off = 0; o->reasoning_len = 0;
|
||||
o->finish_off = 0; o->finish_len = 0;
|
||||
o->choices_span.p = NULL;
|
||||
o->choices_span.len = 0;
|
||||
o->has_choices = 0;
|
||||
o->choices_kind = HA_CHUNK_KIND_ABSENT;
|
||||
o->choices_count = 0;
|
||||
o->choice0_kind = HA_CHUNK_KIND_ABSENT;
|
||||
}
|
||||
|
||||
/* 键名比较:大小写不敏感(struct 字段语义)。
|
||||
* ★ 为什么不直接用 ha_json_key_eq:那个接收 ha_span,而这里要按
|
||||
* 已知长度比较(省掉 strlen)—— 且必须与 Go 对 struct 字段的匹配一致。 */
|
||||
static int ci_eq(const char *p, const char *name, size_t n) {
|
||||
size_t i;
|
||||
for (i = 0; i < n; i++) {
|
||||
if (sse_lower((unsigned char)p[i]) != sse_lower((unsigned char)name[i])) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int kind_of(const ha_span *v) {
|
||||
if (v == NULL || v->p == NULL || v->len == 0) {
|
||||
return HA_CHUNK_KIND_ABSENT;
|
||||
}
|
||||
switch (v->p[0]) {
|
||||
case '"': return HA_CHUNK_KIND_STRING;
|
||||
case '{': return HA_CHUNK_KIND_OBJECT;
|
||||
case '[': return HA_CHUNK_KIND_ARRAY;
|
||||
case 'n': return HA_CHUNK_KIND_NULL;
|
||||
default: return HA_CHUNK_KIND_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* 把字符串值解码进 sbuf 的 [off,off+len)。返回 0 失败(空间不足/语法错)。 */
|
||||
static int emit_decoded(ha_span val, char *sbuf, size_t scap, size_t *off,
|
||||
size_t *outlen) {
|
||||
ha_json_scan sc;
|
||||
ha_span raw;
|
||||
size_t n;
|
||||
|
||||
*off = 0;
|
||||
*outlen = 0;
|
||||
ha_json_scan_init(&sc, val.p, val.len);
|
||||
if (!ha_json_scan_string(&sc, &raw)) {
|
||||
return 0;
|
||||
}
|
||||
/* 上界检查:每个输入字节最坏变 3 字节 U+FFFD */
|
||||
if (scap < raw.len * 3u + 4u) {
|
||||
return 0;
|
||||
}
|
||||
n = ha_json_decode_string_into(raw, sbuf, scap);
|
||||
if (n == (size_t)-1) {
|
||||
return 0;
|
||||
}
|
||||
*off = 0;
|
||||
*outlen = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 顶层单趟分派:遍历成员表一次,按名字分派到对应槽位。
|
||||
* 顶层键(choices/usage)是 **struct 字段** ⇒ 大小写不敏感。
|
||||
* 返回 0 = 正常(即使有重复键,dup 由调用方检查);-1 = 畸形。
|
||||
*/
|
||||
static int chunk_top_dispatch(ha_span root, ha_chunk_out *out, int *dup) {
|
||||
ha_json_members m;
|
||||
ha_span k, v;
|
||||
ha_span el_tmp;
|
||||
int r;
|
||||
|
||||
*dup = 0;
|
||||
if (!ha_json_members_init(&m, root.p, root.len)) {
|
||||
return -1;
|
||||
}
|
||||
while (ha_json_members_next(&m, &k, &v)) {
|
||||
int t = kind_of(&v);
|
||||
if (k.len == 7 && ci_eq(k.p, "choices", 7)) {
|
||||
if (out->choices_kind != HA_CHUNK_KIND_ABSENT) {
|
||||
*dup = 1; /* 重复键:字段级合并语义 ⇒ 交回 Go */
|
||||
}
|
||||
out->has_choices = (t != HA_CHUNK_KIND_ABSENT &&
|
||||
t != HA_CHUNK_KIND_NULL) ? 1 : 0;
|
||||
out->choices_kind = t;
|
||||
out->choices_span = v;
|
||||
if (t == HA_CHUNK_KIND_ARRAY) {
|
||||
/* 一趟同时得出「首元素 span」与「元素个数」。
|
||||
* ★ 初版为了拿个数先把整个数组扫一遍、再调 ha_sse_arr_first
|
||||
* 重新扫第二遍 —— 而单趟成员遍历实测 107ns,两趟就是白扔 100ns+。
|
||||
*/
|
||||
if (scan_first_and_count(&v, &el_tmp, &out->choices_count) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (out->choices_count > 0) {
|
||||
out->choice0_span = el_tmp;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (k.len == 5 && ci_eq(k.p, "usage", 5)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_USAGE].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
*dup = 1;
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_USAGE].span = v;
|
||||
out->slot[HA_CHUNK_SLOT_USAGE].kind = t;
|
||||
continue;
|
||||
}
|
||||
/* 其余顶层键(id/object/created/model/system_fingerprint…)一律忽略。
|
||||
* ★ Go 侧 struct 未声明 ⇒ 忽略;没有「类型不符」的可能。 */
|
||||
}
|
||||
r = ha_json_members_complete(&m) ? 0 : -1;
|
||||
return r;
|
||||
}
|
||||
|
||||
/* 一趟取数组的首元素 span 与元素个数。
|
||||
* 返回 0 成功;非 0 表示数组畸形。
|
||||
* ★ 超过 2 个元素即停止计数并置 *count = 2(调用方一律回退),
|
||||
* 这样超大数组不会白扫 —— 而 Go 侧那种输入压根不该走快速路径。 */
|
||||
static int scan_first_and_count(const ha_span *arr, ha_span *first, int *count) {
|
||||
ha_json_scan sc;
|
||||
int n = 0;
|
||||
first->p = NULL;
|
||||
first->len = 0;
|
||||
ha_json_scan_init(&sc, arr->p, arr->len);
|
||||
(void)ha_json_scan_ws(&sc);
|
||||
if (ha_json_scan_eof(&sc) || sc.s[sc.i] != '[') {
|
||||
return -1;
|
||||
}
|
||||
sc.i++;
|
||||
for (;;) {
|
||||
size_t start;
|
||||
(void)ha_json_scan_ws(&sc);
|
||||
if (ha_json_scan_eof(&sc) || sc.s[sc.i] == ']') {
|
||||
break;
|
||||
}
|
||||
start = sc.i;
|
||||
if (!ha_json_skip(&sc)) {
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
first->p = arr->p + start;
|
||||
first->len = sc.i - start;
|
||||
}
|
||||
n++;
|
||||
if (n >= 2) {
|
||||
/* 已知 >1:调用方必然回退,无需继续扫 */
|
||||
*count = 2;
|
||||
return 0;
|
||||
}
|
||||
(void)ha_json_scan_ws(&sc);
|
||||
if (ha_json_scan_eof(&sc)) {
|
||||
return -1;
|
||||
}
|
||||
if (sc.s[sc.i] == ',') {
|
||||
sc.i++;
|
||||
continue;
|
||||
}
|
||||
if (sc.s[sc.i] == ']') {
|
||||
break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
*count = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* choice0 内的单趟分派:delta + finish_reason(struct 字段 ⇒ CI)。
|
||||
* 返回 0 正常;非 0 = 畸形或重复键。 */
|
||||
static int chunk_choice_dispatch(ha_span choice, ha_chunk_out *out) {
|
||||
ha_json_members cm;
|
||||
ha_span ck, cv;
|
||||
|
||||
if (!ha_json_members_init(&cm, choice.p, choice.len)) {
|
||||
return -1;
|
||||
}
|
||||
while (ha_json_members_next(&cm, &ck, &cv)) {
|
||||
int t = kind_of(&cv);
|
||||
if (ck.len == 5 && ci_eq(ck.p, "delta", 5)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_DELTA].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
return -1; /* 重复键 */
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_DELTA].span = cv;
|
||||
out->slot[HA_CHUNK_SLOT_DELTA].kind = t;
|
||||
continue;
|
||||
}
|
||||
if (ck.len == 13 && ci_eq(ck.p, "finish_reason", 13)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_FINISH_REASON].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
return -1;
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_FINISH_REASON].span = cv;
|
||||
out->slot[HA_CHUNK_SLOT_FINISH_REASON].kind = t;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return ha_json_members_complete(&cm) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* delta 内单趟分派。delta 是 **struct** ⇒ 字段名大小写不敏感。 */
|
||||
static int chunk_delta_dispatch(ha_span delta, ha_chunk_out *out, int *dup) {
|
||||
ha_json_members m;
|
||||
ha_span k, v;
|
||||
|
||||
*dup = 0;
|
||||
if (!ha_json_members_init(&m, delta.p, delta.len)) {
|
||||
return -1;
|
||||
}
|
||||
while (ha_json_members_next(&m, &k, &v)) {
|
||||
int t = kind_of(&v);
|
||||
if (k.len == 7 && ci_eq(k.p, "content", 7)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_CONTENT].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
*dup = 1;
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_CONTENT].span = v;
|
||||
out->slot[HA_CHUNK_SLOT_CONTENT].kind = t;
|
||||
continue;
|
||||
}
|
||||
if (k.len == 17 && ci_eq(k.p, "reasoning_content", 17)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_REASONING].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
*dup = 1;
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_REASONING].span = v;
|
||||
out->slot[HA_CHUNK_SLOT_REASONING].kind = t;
|
||||
continue;
|
||||
}
|
||||
if (k.len == 10 && ci_eq(k.p, "tool_calls", 10)) {
|
||||
if (out->slot[HA_CHUNK_SLOT_TOOL_CALLS].kind != HA_CHUNK_KIND_ABSENT) {
|
||||
*dup = 1;
|
||||
}
|
||||
out->slot[HA_CHUNK_SLOT_TOOL_CALLS].span = v;
|
||||
out->slot[HA_CHUNK_SLOT_TOOL_CALLS].kind = t;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return ha_json_members_complete(&m) ? 0 : -1;
|
||||
}
|
||||
|
||||
int ha_sse_chunk_locate(const char *data, size_t len, ha_chunk_out *out,
|
||||
char *sbuf, size_t scap, size_t *sused) {
|
||||
ha_span root;
|
||||
int dup = 0;
|
||||
ha_span el, v;
|
||||
|
||||
if (out == NULL || sused == NULL) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
chunk_out_reset(out);
|
||||
*sused = 0;
|
||||
if (data == NULL || len == 0) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
root.p = data;
|
||||
root.len = len;
|
||||
|
||||
/* 顶层必须是「恰好一个」良构对象(含尾部残留检查) */
|
||||
if (!ha_sse_root_object(&root)) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
|
||||
if (chunk_top_dispatch(root, out, &dup) != 0) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
if (dup) {
|
||||
return HA_CHUNK_FALLBACK; /* §5.1 字段级合并 */
|
||||
}
|
||||
|
||||
/* ---- choices[0] ---- */
|
||||
if (out->choices_kind == HA_CHUNK_KIND_ARRAY) {
|
||||
if (out->choices_count > 1) {
|
||||
/* Go 侧会解析**全部**元素;本层只认 [0],其余元素可能类型不符
|
||||
* 而让 Go 整块作废 ⇒ 无法保证等价,必须回退。 */
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
if (out->choices_count == 0) {
|
||||
out->choice0_kind = HA_CHUNK_KIND_ABSENT;
|
||||
} else {
|
||||
if (ha_sse_arr_first(&out->choices_span, &el) != 1) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
out->choice0_kind = kind_of(&el);
|
||||
if (out->choice0_kind != HA_CHUNK_KIND_OBJECT) {
|
||||
/* Go 侧是 []struct:元素非对象 ⇒ 整块作废 */
|
||||
return HA_CHUNK_TYPE_FAIL;
|
||||
}
|
||||
/* 元素内的 delta / finish_reason(struct 字段 ⇒ CI),**一趟**取完。
|
||||
* ★ 初版这里对 choices 数组做了「数个数 + 取首元素」两趟、
|
||||
* 又在 choice0 内单独跑一趟 members —— 合计 3 趟。
|
||||
*/
|
||||
{
|
||||
if (chunk_choice_dispatch(el, out) != 0) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- delta 内分派 ---- */
|
||||
if (out->slot[HA_CHUNK_SLOT_DELTA].kind == HA_CHUNK_KIND_OBJECT) {
|
||||
v = out->slot[HA_CHUNK_SLOT_DELTA].span;
|
||||
if (chunk_delta_dispatch(v, out, &dup) != 0) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
if (dup) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
} else if (out->slot[HA_CHUNK_SLOT_DELTA].kind == HA_CHUNK_KIND_OTHER) {
|
||||
/* delta 非对象:Go 侧 Unmarshal 到 struct 会失败 */
|
||||
return HA_CHUNK_TYPE_FAIL;
|
||||
}
|
||||
|
||||
/* ---- content:字符串直接解码;文本数组走 stringify ---- */
|
||||
{
|
||||
ha_chunk_slot *cs = &out->slot[HA_CHUNK_SLOT_CONTENT];
|
||||
if (cs->kind == HA_CHUNK_KIND_STRING) {
|
||||
if (!emit_decoded(cs->span, sbuf, scap, &out->content_off,
|
||||
&out->content_len)) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
*sused = out->content_len;
|
||||
} else if (cs->kind == HA_CHUNK_KIND_ARRAY) {
|
||||
size_t n = 0;
|
||||
if (!ha_sse_stringify(&cs->span, sbuf, scap, &n)) {
|
||||
/* 需 json.Marshal 重新编码(§5.2)⇒ 交回 Go */
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
out->content_off = 0;
|
||||
out->content_len = n;
|
||||
*sused = n;
|
||||
} else if (cs->kind == HA_CHUNK_KIND_OBJECT ||
|
||||
cs->kind == HA_CHUNK_KIND_OTHER) {
|
||||
/* 对象/数字/布尔 ⇒ stringifyContent 走 json.Marshal(§5.2) */
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
/* NULL / ABSENT ⇒ content=""(与 Go 的 stringifyContent(nil) 一致) */
|
||||
}
|
||||
|
||||
/* ---- reasoning_content:Go 侧是 **string**(强类型) ----
|
||||
* 若是 string 则解码;若是 null/absent ⇒ "";其它类型 ⇒ 整块作废。 */
|
||||
{
|
||||
ha_chunk_slot *rs = &out->slot[HA_CHUNK_SLOT_REASONING];
|
||||
if (rs->kind == HA_CHUNK_KIND_STRING) {
|
||||
if (!emit_decoded(rs->span, sbuf + *sused, scap - *sused,
|
||||
&out->reasoning_off, &out->reasoning_len)) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
out->reasoning_off += *sused;
|
||||
*sused += out->reasoning_len;
|
||||
} else if (rs->kind != HA_CHUNK_KIND_ABSENT &&
|
||||
rs->kind != HA_CHUNK_KIND_NULL) {
|
||||
return HA_CHUNK_TYPE_FAIL; /* 与 Go 的 Unmarshal 失败一致 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- finish_reason:Go 侧是 *string ---- */
|
||||
{
|
||||
ha_chunk_slot *fs = &out->slot[HA_CHUNK_SLOT_FINISH_REASON];
|
||||
if (fs->kind == HA_CHUNK_KIND_STRING) {
|
||||
if (!emit_decoded(fs->span, sbuf + *sused, scap - *sused,
|
||||
&out->finish_off, &out->finish_len)) {
|
||||
return HA_CHUNK_FALLBACK;
|
||||
}
|
||||
out->finish_off += *sused;
|
||||
*sused += out->finish_len;
|
||||
} else if (fs->kind != HA_CHUNK_KIND_ABSENT &&
|
||||
fs->kind != HA_CHUNK_KIND_NULL) {
|
||||
return HA_CHUNK_TYPE_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
return HA_CHUNK_OK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user