Files
HomeAgent/csrc/bench/bench_ha_codec.c
JianFeeeee 8070844fe9 build(csrc): C 基础设施门禁(ABI 版本 / 告警 / sanitizer / 交叉编译 / 模糊测试)
C 化从「一把刀」推进到「可持续推进」,本轮先把基础设施建起来:
不建它,后续每个 C 切片都在裸奔(无告警门禁、无内存安全检查、
无交叉编译验证、无 ABI 漂移检测)。

新增门禁(make check-csrc / check-csrc-full,已接进 make test):
- csrc-lint   gcc+clang 双编译器 × -Wall -Wextra -Wpedantic -Wshadow
              -Wconversion,零告警才算过(-Wconversion 是为 cgo 窄化
              准备的:size_t→int 截断在默认档下是静默的)
- csrc-abi    ABI 版本运行期自述 + 荒谬值检查
- csrc-headers 头文件自包含性(每个 .h 能单独编过)
- csrc-sanitize ASan+UBSan 跑 C 契约测试
- csrc-cross  arm64 交叉编译(homed 的真实发布目标)
- csrc-fuzz   libFuzzer:内存安全 + 6 条不变式(可 CI 门禁)

新增 ABI 契约(csrc/include/ha_abi.h):
- ha_codec.h 声明「签名冻结」,但冻结只写在注释里;现改为
  HA_CODEC_ABI_MAJOR/MINOR + 运行期自述 + Go 侧常量,
  三方交叉断言,版本漂移在测试期判红而非线上表现为行为诡异。
- 门禁当场抓出我自己的两个真 bug:①_Static_assert 是 C11 而项目
  是 -std=c99(-Wpedantic 报的);②##msg 不能拼接字符串字面量,
  导致两个断言共用一个 typedef 名(clang 报的)。

基础设施当场抓出的三个真实缺陷(都是「本机 gcc 能编过、别处会炸」类):
- bench 用了 POSIX clock_gettime,而 CMake 刻意 C_EXTENSIONS OFF
  (严格 c99)→ 头文件未声明;补 _POSIX_C_SOURCE(须在任何头之前)
- 头文件缺 include 时只在「恰好被别的头先包含」处静默编过
- Go 不允许在 _test.go 用 cgo ⇒ C 侧 const 桥接只能放非测试文件,
  且 cgo 生成的 *_Cvar_* 不是 Go 常量(「两边一起错成一样」的盲区,
  改用 C 函数返回 + 编译期 _Static_assert 补上)

实测(全部当场可复现):
- 双编译器 × c99/c11 零告警;ASan+UBSan PASS
- libFuzzer 91s 跑 3329316 次、零崩溃(6 条不变式全过)
- 变异测试:改 C 侧宏 / 让 Go 常量与 C 函数「一起错成一样」,
  均被对应断言抓住(证明门禁不是摆设)
- C 侧纯函数基准(首次把函数体成本与 cgo 边界成本分开测):
  ascii_1k 121.7ns/8.4GB/s;zh_1k 1434ns;truncate 两者均约 128-134ns
- 全量 go test -count=1 ./...:57 包 0 FAIL
- make build-linux-arm64 → ELF aarch64
- 纪律检查 FAIL=0;SDK 公开接口 diff = 0 行

未动:csrc/ 是内核 C ABI,不属 third_party/homeagent-sdk 公开接口。
2026-09-26 09:44:17 +08:00

176 lines
6.3 KiB
C
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.

/*
* bench_ha_codec.c —— C 侧纯函数微基准(无 cgo 边界成本)
*
* ============================ 为什么 Go 侧基准不够 ============================
* Go 侧 codec_bench_test.go 测到的数 = **函数体成本 + cgo 边界成本**(约 30ns)
* 两项混在一起。后果:看到某个场景慢,分不清该优化 C 函数体,还是该减少
* 跨语言调用次数(或把循环整体 C 化批量传一次)—— 而这三者的处方完全不同。
* 只有在能隔离边界成本的地方(纯 C 循环)测,才知道该动谁。
*
* 用法:cmake -DBUILD_BENCH=ON && ./ha_codec_bench [reps]
*
* 覆盖与 Go 侧 benchInputs 对齐(empty / ascii_short / zh_short / zh_200 /
* ascii_1k / zh_1k),便于两张表直接对读。
*/
#ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 199309L
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ha_codec.h"
/* 单调时钟(纳秒)。
*
* ★ 必须是 clock_gettime(不是 clock()、不是 time()):基准要测的是
* 几十纳秒级的函数体耗时,clock()/time() 的分辨率是**秒**,
* 拿它测 ns/op 只会得到一堆 0 或被量化成整数秒的噪声。
*
* ★ CLOCK_MONOTONIC 与 clock_gettime 都是 POSIX 而**非 ISO C99**,
* 而 CMake 刻意设了 CMAKE_C_EXTENSIONS OFF(严格 -std=c99)
* ⇒ 未定义这两个符号。实测报错:
* error: storage size of 'ts' isn't known
* error: implicit declaration of function 'clock_gettime'
* 这是 C 化门禁当场抓出的真实可移植性缺陷 —— 若靠 Makefile 的裸 gcc
* (默认 gnu17)构建,它会**静默编过**;而到别人的严格 C99 工具链上就炸。
*
* 故显式请求 POSIX 声明。_POSIX_C_SOURCE 必须在包含任何头文件**之前**
* 定义(否则 feature test macro 无效,这也是最常见的踩法)。
* Windows/MSVC 走 _MSC_VER 分支(用 QueryPerformanceCounter),
* 保证这个 bench 文件在异端也能编。 */
#if defined(_MSC_VER)
# include <windows.h>
static double now_sec(void) {
LARGE_INTEGER f, c;
QueryPerformanceFrequency(&f);
QueryPerformanceCounter(&c);
return (double)c.QuadPart / (double)f.QuadPart;
}
#else
# ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 199309L
# endif
# include <time.h>
static double now_sec(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
#endif
/* 分配并填充 reps 个 'x' 的缓冲(可含 NUL 之外的任意字节)。 */
static char *make_fill(size_t n, char ch) {
char *p = (char *)malloc(n ? n : 1);
if (p) memset(p, ch, n);
return p;
}
static void bench_estimate(const char *name, const char *s, size_t len, int reps) {
/* 预热:把指令缓存与分支预测器带进稳态,否则首个样本的冷启动会
* 均摊到很少的迭代上(reps 小的时候误差极大)。 */
for (int i = 0; i < reps; i++) (void)ha_codec_estimate_tokens(s, len);
double t0 = now_sec();
int acc = 0;
for (int i = 0; i < reps; i++) {
acc += ha_codec_estimate_tokens(s, len);
}
double dt = now_sec() - t0;
double ns = (reps > 0) ? (dt * 1e9 / reps) : 0.0;
double mbs = (dt > 0) ? ((double)len * reps / dt / 1e6) : 0.0;
printf(" %-12s len=%7zu %9.2f ns/op %8.1f MB/s (acc=%d)\n",
name, len, ns, mbs, acc);
}
static void bench_truncate(const char *name, const char *s, size_t len,
int max_tokens, int reps) {
for (int i = 0; i < reps; i++) {
(void)ha_codec_truncate_by_tokens(s, len, max_tokens);
}
double t0 = now_sec();
size_t acc = 0;
for (int i = 0; i < reps; i++) {
acc += ha_codec_truncate_by_tokens(s, len, max_tokens);
}
double dt = now_sec() - t0;
double ns = (reps > 0) ? (dt * 1e9 / reps) : 0.0;
printf(" %-12s len=%7zu %9.2f ns/op (keep=%zu)\n",
name, len, ns, acc / (size_t)reps);
}
int main(int argc, char **argv) {
int reps = (argc > 1) ? atoi(argv[1]) : 200000;
if (reps <= 0) reps = 200000;
printf("== ha_codec C 侧微基准(reps=%d,纯 C 无 cgo 边界)==\n", reps);
printf("-- ha_codec_estimate_tokens --\n");
bench_estimate("empty", "", 0, reps);
bench_estimate("ascii_short", "hello world", 11, reps);
bench_estimate("zh_short", "用户询问了系统状态", 27, reps);
{
char *zh200 = make_fill(180, 'a'); /* 逐字节非 ASCII 由下方覆盖 */
bench_estimate("ascii_200", zh200, 180, reps);
free(zh200);
}
{
char *zh = make_fill(1024, 'x');
bench_estimate("ascii_1k", zh, 1024, reps);
free(zh);
}
{
/* 真实中文:每字 3 字节 = 1024 字节 ≈ 341 rune */
char *zh = make_fill(1023, 'x');
for (size_t i = 0; i + 2 < 1024; i += 3) {
zh[i] = (char)0xE4; zh[i + 1] = (char)0xBD; zh[i + 2] = (char)0xA0;
}
bench_estimate("zh_1k", zh, 1024, reps);
free(zh);
}
printf("-- ha_codec_truncate_by_tokens (max_tokens=64) --\n");
{
char *a1k = make_fill(1024, 'x');
bench_truncate("ascii_1k", a1k, 1024, 64, reps);
free(a1k);
}
{
char *zh = make_fill(1023, 'x');
for (size_t i = 0; i + 2 < 1024; i += 3) {
zh[i] = (char)0xE4; zh[i + 1] = (char)0xBD; zh[i + 2] = (char)0xA0;
}
bench_truncate("zh_1k", zh, 1024, 64, reps);
free(zh);
}
printf("-- ha_codec_model_context_window (含/不含匹配) --\n");
{
const char *models[4] = {
"deepseek/deepseek-v4.1-flash", "gpt-4-turbo", "qwen-max", "AUTO"
};
for (int w = 0; w < 4; w++) {
size_t l = strlen(models[w]);
for (int i = 0; i < reps; i++) {
(void)ha_codec_model_context_window(models[w], l);
}
double t0 = now_sec();
int acc = 0;
for (int i = 0; i < reps; i++) {
acc += ha_codec_model_context_window(models[w], l);
}
double dt = now_sec() - t0;
printf(" %-30s %9.2f ns/op (win=%d)\n", models[w],
(reps > 0) ? dt * 1e9 / reps : 0.0, acc / reps);
}
}
printf("-- ABI --\n");
printf(" ha_codec_abi_version = %d\n", ha_codec_abi_version());
return 0;
}