V 语言自编译性能追踪:读懂并部署 cmd/tools/fast "Is V still fast?" 基准仪表盘
【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C => V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v
本文围绕 V 编译器官方基准工具 cmd/tools/fast 展开:它测量指定提交下 V 编译器编译自身(以及若干小程序)的耗时,通过V 的 ORM将每次测量写入本地SQLite数据库(fast.db),再由一个轻量vebWeb 应用把历史趋势可视化出来。读完本文,你将掌握该工具的全部子命令用法、历史数据迁移流程、按年/按步长批量回测历史提交的方法,以及如何把它接入 cron 定时采样,复刻出与官方 "Is V still fast?" 仪表盘一致的完整链路。
一、项目定位与源码结构
fast官方 README 的定位非常明确:它是线上编译性能仪表盘背后的引擎,测量“给定提交下 V 编译自身要花多久”,同时测量编译hello_world.v这类小程序的开销。所有测量结果通过 V ORM 持久化进SQLite,前端为一个用veb编写的小型 Web 应用。
从 目录结构 看,它由以下几个 V 源文件和一个模板构成:
| 文件 | 职责 |
|---|---|
| cmd/tools/fast/fast.v | 主分发器(main),共享配置常量,seed子命令 |
| cmd/tools/fast/models.v | BenchmarkORM 模型、SQLite 建表/迁移,以及视图与差值(delta)计算逻辑 |
| cmd/tools/fast/bench.v | 测量引擎 +bench子命令 |
| cmd/tools/fast/runner.v | “一年中每 N 个提交采样一次”的采样器 +run/remeasure子命令 |
| cmd/tools/fast/server.v | veb Web 应用(首页、JSON API、健康检查) |
| cmd/tools/fast/import.v | 把旧版静态站点历史迁移进fast.db的import子命令 |
| cmd/tools/fast/export.v | 导出静态站点的export子命令 |
| cmd/tools/fast/templates/index.html | 仪表盘 UI(图表 + 表格) |
fast.db | SQLite 数据库(已加入 git 忽略,首次运行自动创建) |
fast.v的main()是一个典型的命令分发器:读取args[1]作为子命令,并把args[2..]交给对应实现,默认子命令是serve。从源码可以看到命令还有别名(server/web等同serve,run-2026/backfill等同run),未知命令会打印帮助并以退出码 1 结束:
cmd := if args.len > 1 { args[1] } else { 'serve' } rest := if args.len > 2 { args[2..] } else { []string{} } match cmd { 'serve', 'server', 'web' { serve(rest) or { fatal('serve failed: ${err}') } } 'bench' { cmd_bench(rest) or { fatal('bench failed: ${err}') } } 'run', 'run-2026', 'backfill' { cmd_run(rest) or { fatal('run failed: ${err}') } } 'remeasure' { cmd_remeasure(rest) or { fatal('remeasure failed: ${err}') } } 'seed' { cmd_seed() or { fatal('seed failed: ${err}') } } 'import' { cmd_import(rest) or { fatal('import failed: ${err}') } } 'export' { cmd_export(rest) or { fatal('export failed: ${err}') } } 'help', '-h', '--help' { print_help() } // ... }几个关键路径常量定义在文件顶部(fast.v):
fast_dir= 本文件所在目录,即cmd/tools/fast/;vdir= V 仓库根目录(向上三级);db_path=cmd/tools/fast/fast.db;log_path=cmd/tools/fast/fast.log。
所有耗时日志都会通过elog()追加写入fast.log,并同步打向 stdout。
二、子命令速览与编译方式
官方 README 要求在cmd/tools/fast/目录内部执行以下命令:
v run . serve [-port 8080] # 启动 Web 应用(默认命令) v run . bench [-clang] [-noprod] # 对当前 HEAD 提交做基准测试 v run . run [-year 2026] [-step 50] [-latest N] [-branch <ref>] [-dry-run] # 对一年中每 <step> 个提交采样测一次, # 或用 -latest 只测最近 N 个提交 v run . remeasure # 对库中每个已存提交重新测量(如回填 RSS 等新指标) v run . export [-o <dir>] # 渲染静态站点(index.html + json) v run . seed # 插入演示数据行,用于预览 UI v run . import [--since YYYY-MM-DD] [--ref <ref>] <table.html> [...] # 把旧 fast.vlang.io 历史迁移进 fast.db v run . helpv run .每次都会经过编译,若想长期复用可先编译一次得到独立二进制,此后直接用二进制调用,速度更快:
v -o fast . ./fast serve各子命令的语义对应 fast.v 中的cmd_*函数,分别负责:启动 veb(serve)、测量当前 HEAD(bench)、遍历历史采样(run)、全库重测(remeasure)、渲染静态站(export)、注入演示数据(seed)、解析并导入旧 HTML 表格(import)。
2.1serve:启动本机仪表盘
serve先确保数据库与benchmarks表存在(调用open_db()后立即关闭连接),再绑定到localhost启动 veb 应用——源码注释明确说明它只服务于本地场景:
veb.run_atApp, Context!默认端口 8080,可用-port覆盖。启动后打开http://localhost:8080即可看到图表与历史表格。服务暴露了以下路由(见 server.v):
/渲染首页(图表 + 完整基准历史表格);/benchmarks.json与/api/benchmarks:输出图表 JSON 数据(两种路径共享同一实现,方便静态导出与动态服务两种模式复用);/health:健康检查,返回纯文本ok,适合作为后台任务探活。
2.2bench:测量当前 HEAD
bench解析当前HEAD的短哈希(前 8 位)、提交标题与提交者时间(%ct,注意不是作者时间%at——提交者时间在 first-parent 历史上是单调的,按它排序能保证与血缘顺序一致),然后执行完整流程:
- 获取全局构建锁(与
run共用同一把锁,见下文); claim_history声明/校验数据库所属的唯一 git 历史;- 若该提交已存在则跳过(除非传
-force强制重测); build_vprod用当前仓库自己的./v构建一个优化的vprod二进制(-prod模式;-noprod可跳过优化、加速构建,适合快速验证);run_measurements跑完整测量套件;- 通过
upsert原子写入一行Benchmark。
默认使用系统 C 编译器cc(-clang则改用 clang)。注意 bench.v 中特意不用-prealloc:在历史提交上预分配分配器可能启动即崩溃(SIGBUS),会让每次“测量”都变成瞬时崩溃而不是真实编译。构建前还会先删除残留的旧vprod,避免“构建失败却拿旧二进制冒名顶替新提交”的脏数据。
2.3run:按年/步长批量回测历史
这是整个工具被设计出来的核心诉求——在本地机器上对 2026 年每第 50 个提交做基准:
./fast run # == ./fast run -year 2026 -step 50采样范围从仓库默认分支解析而来(origin/HEAD,回退到本地master/main)。由于每个数据库只追踪一条git 历史,若处于 detached HEAD 或浅克隆、上述引用都无法解析,就必须显式传-branch <ref>;无法解析的HEAD会被拒绝而不是被当作可追踪历史(这是为了避免把互不相关的 detached 提交全部归入HEAD历史而污染按血缘绘制的图表,详见claim_history的校验逻辑)。
先用-dry-run预览将采到的精确提交集合,不构建任何东西、不触碰数据库:
./fast run -dry-run源码中-dry-run分支只枚举提交并打印[序号/总数] 短哈希 日期 标题,随后直接返回。其边界处理值得一提:年份边界锚定在当天午夜(${year}-01-01T00:00:00形式不含空格,shell 安全),避免git把裸日期按当前时刻解析导致 1 月 1 日早上的提交被漏掉、采样点整体漂移;且从第step个提交开始取(索引step-1起跳),保证“每 50 个提交”真正取到第 50、100、150… 个,而不是第 1、51、101… 个。
-latest N模式则绕过年份逻辑,直接取最近 N 个 first-parent 提交(倒序后按旧到新存储)。对每个被采样的提交,run依次执行:
- 用oldv工具(cmd/tools/oldv.v)重建“与该提交完全一致”的 V——它会自动定位匹配的
vc引导提交、准备好 tcc,并在~/.cache/oldv/v_at_<commit>/下自编译出可用的./v,完全不触碰你正在使用的仓库主检出; - 在该历史检出中构建优化版
vprod; - 运行测量,写入一行
Benchmark到fast.db。
首次运行还会把vlang/v与vlang/vc克隆进~/.cache/oldv/;之后历史构建都有缓存,重跑时只补测缺失的提交。该流程天然幂等——数据库中已存在的提交会被跳过,因此可以随时中断、随时续跑。代价是:完整 bootstrap 构建 + 每项测量约 20 个样本,跑 56 个历史提交是相当重的任务,请预留充足时间,进度日志写在fast.log。
三、迁移旧站点历史:import
旧版工具把历史累积在table.html(镜像到 gh-pages 分支的index.html)。在把fast.db变成权威数据源之前,应先导入这段历史,避免仪表盘从零开始:
# 在旧 gh-pages 输出的检出目录里执行 ./fast import path/to/table.html # 或旧的 index.html ./fast import 2024.html 2023.html 2022.html # 也可以传多个逐年归档文件 # 只导入某日期之后的行: ./fast import --since 2026-01-01 index.htmlimport会解析旧表格中14 列的行(timestamp、commit、message、v.c、v、…),逐行插入。它是幂等的——已在数据库中的提交会被跳过——所以可以重复执行,或一次指向多个文件。
源码层面的几个严谨细节值得注意(import.v):
- 只接受真实十六进制提交哈希(7~40 位 hex),防止恶意 HTML 把 shell 元字符注入后续
git/oldv命令; - 导入的 ID 会被
resolve_commit统一解析为 8 位规范短哈希 + 提交者时间;如果某提交不在本地检出中(无法保证血缘顺序),直接跳过该行; - 插入前用
commit_off_history校验提交确实属于被声明的历史(防止拿错归档配上错的--ref);而若声明的 ref 本身无法解析,则直接失败而不是照单全收——避免一个不存在的标识符把所有无关归档静默导入; - 历史归属校验放在一个事务里:若最终插入了 0 行且此前数据库未声明历史,则回滚声明(
ROLLBACK),避免空数据库被一个无效导入永久占坑。
3.1 多历史与--ref
每个数据库只追踪一条 git 历史,导入的行都会被标记上这条历史,混用分支会被拒绝。默认使用仓库默认分支(旧仪表盘追踪的正是它)。如果要迁移的是另一条历史,请传--ref <ref>(例如--ref origin/v3),这样之后针对该分支的run/bench才被接受,而无关分支会被拒绝。
所谓“历史身份”,在 models.v 中通过normalize_ref+claim_history实现:
normalize_ref把master与origin/master这类同一条血缘的分支折叠为同一身份,但不会合并不同远端上的同名分支(origin/release与upstream/release保持区分);只有两分支互为祖先(在一条历史线上)时才会合并,真正分叉的本地分支保留自己的身份,绝不会被悄悄并入远端的版本序列;claim_history用INSERT OR IGNORE在空数据库上原子抢占身份(并发竞态时只有一个赢家);若已存的history_ref与本次不同则拒绝;- 更进一步,它用
history_diverged检查“最新已存提交是否仍是本 ref 的祖先”:若该分支曾被 force-push/reset 到无关历史,继续追加会让仪表盘对比互不相关的版本,因此同样拒绝。
3.2 回填 RSS 数据:remeasure
旧数据没有内存数字,导入后每行的 RSS 字段全是 0,仪表盘上的 "RSS: self-compile"/"RSS: hello.v" 图表会是空的。导入之后(以及任何向既有部署新增指标时),运行remeasure重新测量库中已存的每个提交来填充它们:
./fast remeasure # 复用缓存的 oldv 构建;安全,可随时停止/续跑remeasure遍历全部已存行,把每个短哈希解析回完整哈希以复用 oldv 缓存构建,然后重新测量并upsert(保持原git_ref,单条原子语句覆盖,即使失败也不会丢失旧测量结果)。
四、接入自动化
4.1 本地动态仪表盘
按计划采样并保持 veb 服务常驻。注意fast run是从本检出枚举提交的,因此必须在 cron 里先git pull更新检出——oldv 缓存同步只刷新~/.cache/oldv,不更新仓库本身;不 pull 的话仪表盘就会停止前进。
# 每小时:更新检出、重新编译工具、采样新提交。 0 * * * * cd /path/to/v && git pull --ff-only \ && cd cmd/tools/fast && v -o fast . && ./fast run >> fast.log 2>&1服务端放在 tmux/screen 或用户级 systemd service 中常驻即可:
cd /path/to/v/cmd/tools/fast && ./fast serve -port 80804.2 发布线上静态站点
fast run只写fast.db,不会更新线上站点——线上站点由 GitHub Pages 静态托管。因此还需要export导出静态站点并推送。把$SITE指向生成站分支的检出(github.com/vlang/website的gh-pages分支)。仓库中的fast_pages.yml工作流会拉取该分支并从自托管 macOS runner 部署,同时下面这条 cron 每小时兜底执行:
# 每小时:采样新提交、重新生成静态站点并发布。 # { diff || commit; } 分组保证 || 只作用于 diff 检查(无变化则不提交); # 整条链用 && 串联并包在 { ...; } 中,任何早期失败(pull/build/run/export) # 都会在 commit/push 之前中止并被日志捕获——绝不会发布过期数据。 0 * * * * { cd /path/to/v && git pull --ff-only && cd cmd/tools/fast && v -o fast . \ && ./fast run \ && ./fast export -o "$SITE" \ && git -C "$SITE" add -A \ && { git -C "$SITE" diff --cached --quiet || git -C "$SITE" commit -m "update fast.vlang.io" ; } \ && git -C "$SITE" push \ && gh workflow run fast_pages.yml -R vlang/v ; } >> /path/to/v/cmd/tools/fast/fast.log 2>&1(旧的fast_job.v守护进程及其-upload步骤已被移除,上面这种 export-and-push 流程就是它们的替代品。)
4.3export:渲染静态站点
export在本地渲染出与 veb/路由完全相同的页面,输出index.html+benchmarks.json两个文件到目标目录(默认当前目录下的site/),供 GitHub Pages 托管;页面上图表请求同目录下的benchmarks.json。需要特别指出的是:veb 的$veb.html()会对插值做 HTML 转义,而静态导出走的是$tmpl(不转义),因此 export.v 在渲染前会手动html.escape每行的 commit message,防止用户可控文本破坏title属性或向发布的静态页注入标记。
五、测量指标定义
| 列 | 命令 | 含义 |
|---|---|---|
v -o v.c | vprod -o v.c cmd/v/ v3 self | 自编译到 C(源文件历史上是cmd/v,2026-07-30 起为vlib/v3/v3.v) |
v -o v | vprod -o v cmd/v/ v3 self | 自编译为二进制 |
V lines / s | 派生值 | 每秒编译的 V 源文件行数(对应v -o v.c步骤) |
V lines | -stats | 被编译的 V 源文件行数 |
v hello.v | vprod examples/hello_world.v | 编译一个小程序 |
v.c size | – | 生成的v.c体积 |
| scan/parse/check/cgen | v3 self-show-timings | 各阶段耗时与 RSS(取最小) |
5.1 计时与抗噪策略
墙钟时间类测量先做若干次热身,再取max_samples个样本、丢弃最慢的若干样本再取平均,以削减随机负载尖峰。相关常量定义在 fast.v:
const warmup_samples = 1 // 热身次数 const max_samples = 8 // 正式样本数 const discard_highest_samples = 3 // 丢弃最慢的样本数(去噪) const rss_samples = 5 // 用于峰值 RSS 五数概括的运行次数 const voptions = ' -skip-unused -show-timings -stats '代码注释透露,早期 AWS 云端环境用的是 2/20/16,如今调低是为了让 56 个本地回测提交在合理时间内跑完。任何一次被测量命令非零退出都会让该提交失败跳过,而不会把崩溃瞬间记成亮眼的成绩——测量前还有一道自编译“探针”:若产生的v.c不存在或小于 100KB,说明该提交编译损坏,直接跳过。
5.2 分阶段计时与 RSS
measure_steps_minimal会对同一编译命令重复max_samples次,每个编译器阶段(SCAN/PARSE/CHECK/C GEN)取最小值。阶段耗时来自-show-timings输出,V 源码行数来自-stats输出的parsed .v lines或V source code size:。输出解析针对新旧两种格式分别处理(parse_stage_measurements判断是否含' MB RSS'来走 v3 分支),bench_test.v 用两段典型输出固化了这一解析逻辑:
- 旧编译器三段式:
24 ms SCAN / 106 ms PARSE / 123 ms CHECK / 75 ms C GEN+V source code size: 196376 lines...; - v3 新格式:
parse setup/cache 6.18 ms 26 MB RSS ...、check 32.65 ms 37 MB RSS ...等,v3 在搭建 parse 流水线时顺带完成扫描,因此parse setup/cache被归并到 SCAN 阶段。
阶段 RSS 的时间分界线是 2026-07-30:从该日期起的提交,自编译测量改用vlib/v3/v3.v(配合独立构建的 v3 驱动二进制fastv3,见build_v3_stage_compiler);更早的行保持只有计时、各 RSS 字段为 0,以保住历史阶段序列的连贯性。峰值 RSS 通过平台time工具采集:Linux 用/usr/bin/time -v(解析Maximum resident set size),macOS 用-l(解析maximum resident set size),每次运行使用含 PID 与单调时钟的临时文件,避免并发采样互相覆盖。每种测量跑rss_samples次后按升序排序,产出 [min, q1, median, q3, max] 五数概括(存为self_rss_*_kb与hello_rss_*_kb两组字段),供前端绘制箱线图。
5.3 表格中的差值(delta)着色
首页表格里每列相对上一行(更旧的一个提交)的差值在 models.v 的build_rows/delta中于服务端算好,模板和浏览器保持“傻瓜化”。毫秒数越小代表编译器越快,因此负差渲染为绿色、正差渲染为红色;差值低于阈值(v.c 为 18ms、v.self 与 hello 为 36ms)视为噪声不显示。每个被比较的行都取commit_date的相邻者,这正解释了为何库内要求日期沿 first-parent 单调、且数据库绝不允许混入两条历史。
六、数据模型与数据库
一切数据都在fast.db(SQLite)中。表结构由 models.v 的Benchmark结构体定义,打开数据库时自动建表。结构体主要字段如下:
| 字段 | 含义 |
|---|---|
commit_hash | 8 位短提交哈希,@[unique]唯一键,保证并发运行不会插入重复行 |
git_ref | 该行所属历史(如origin/master);混用会被拒绝 |
message | 提交标题 |
commit_date | 提交者时间(%ct),沿 first-parent 单调 |
created_at | 实际执行基准的时刻 |
v_c_ms/v_self_ms | 自编译到 C / 自编译为二进制的毫秒数 |
hello_ms | 编译hello_world.v的毫秒数 |
vc_size_kb | 生成v.c的体积(KB) |
scan_ms/parse_ms/check_ms/cgen_ms | 各阶段耗时(最小值) |
scan_rss_kb/parse_rss_kb/check_rss_kb/cgen_rss_kb | 各阶段 RSS(KB) |
vlines/lines_per_s | 编译的源行数 / 每秒行数 |
self_rss_{min,q1,med,q3,max}_kb、hello_rss_{min,q1,med,q3,max}_kb | 峰值 RSS 五数概括 |
6.1 建表、迁移与seed
open_db()用sql db { create table Benchmark }打开(内部映射为CREATE TABLE IF NOT EXISTS,可反复调用),随后执行迁移。数据库有版本号概念:以PRAGMA user_version记录schema_version = 3,迁移在单个事务中执行、幂等且失败即抛错,不会留下半迁移状态(models.v)。- 迁移会为缺失的 RSS 列、
git_ref列补ALTER TABLE,去重同哈希的重复行并创建唯一索引idx_benchmarks_commit_hash,还会建一张fast_meta单行键值表来存放“历史身份”。 seed子命令插入 4 行合成数据用于预览 UI。它非常克制:拒绝非空数据库(防止演示数据混入真实数据),并且给演示行声明一个seed-demo身份,后续真实run/bench因此也会被拒——想预览 UI 请用一个空库或独立库。
用 SQLite 命令行直接查看历史很方便:
sqlite3 fast.db \ 'select commit_hash, commit_date, v_c_ms, v_self_ms from benchmarks order by commit_date;'七、可靠性设计:全局构建锁与缓存
由于run的每个 oldv 构建都复用共享的~/.cache/oldv/v_at_vc检出,而bench会直接在主检出里重写vprod、v.c、v2与 hello 二进制,二者绝不能并行,因此 runner.v 实现了一把全局构建锁:
- 互斥手段是原子的
mkdir(build_lock_dir = ~/.cache/oldv/fast-build.lock),dir_created保证多个竞态进程只有一个成功; - 持锁进程每 5 分钟通过已打开的 owner 文件描述符刷新心跳(
build_lock_heartbeat_secs),超时阈值 1 小时(build_lock_stale_secs),因此单次超过一小时的慢构建不会被误判为崩溃并被回收;心跳通过描述符而非路径写入,即使锁被回收、目录被改名,心跳也只会落到孤儿 inode 上,永远不会覆盖继任者的 owner 文件; - 释放时先核对 owner 文件仍是自己的 token,避免把已被接管的新锁误删。
采样循环中,并发安全由commit_hash的 UNIQUE 约束兜底:若 cron 与手动回测撞车,insert_benchmark会因唯一键冲突被拒并记作 skip,而不是插入重复行。若一次全新数据库上的运行最终一行都没存成功,程序会条件式删除自己刚做的历史声明(单条语句保证与并发导入无竞态),以免空库被永久占坑、后续其它 ref 的运行全部被误拒。
八、小结
cmd/tools/fast是一个完整自洽的“编译器性能回归观测台”:bench负责单点快照,run负责按年/步长/最近 N 个提交做历史回测,import负责无缝承接旧站历史,remeasure负责给历史行回填新指标,serve/export分别覆盖动态仪表盘与静态 Pages 两种发布形态,seed让 UI 无需真实跑分即可预览。底层由 V ORM + SQLite 持久化、veb 提供 Web 层、oldv 完成“穿越式”历史编译器重建,再加上历史身份归一化、幂等 upsert、心跳型全局构建锁等设计,兼顾了正确性、并发安全与断点续跑。如果你想为其它编译器项目搭建类似的“每 N 个提交回测一次”性能看板,这套代码的模块切分与上述可靠性模式都很值得直接借鉴。
【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C => V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考