技术札记

从 v1.7.0 到 v1.7.1:让 HHY 的每一次提交都有边界

从优化编译器回到资源归属、失败恢复和受控并发,复盘 HHY v1.7.1 的工程取舍、真实性能基线,以及走向共享状态、协程与分布式事务的下一步。

HOUHUIYANG.COM

扫码继续阅读

正在生成…

从 v1.7.0 到 v1.7.1:让 HHY 的每一次提交都有边界

houhuiyang.com/zh/notes/evolving-hhy-from-1-7-0-to-1-7-1

写完 v1.7.0 的优化编译器,我又回到了几个看起来没那么“先进”的问题:一个环境究竟由谁保活,一条 Stream 关闭之后还留着什么,一个任务失败时谁负责把子进程收回来,以及一次更新报错之后,数据到底有没有提交。

这些问题没有新语法那么显眼,却决定了我敢不敢让 HHY 持续运行,敢不敢让它修改真实的业务状态。

上一篇文章记录了从资源生命周期到 HIR/MIR 的优化链路。v1.7.1 接着往下走:把资源归属补牢,把几类提交行为的保证说清楚,再用当前二进制建立一份能够继续比较的性能基线。

对我来说,这次迭代最重要的进展,是让“成功、失败、结果未知”分别有自己的含义。 后面的共享状态、协程和分布式事务,都需要建立在这个基础上。

本文依据 2026 年 9 月 14 日的语言现状报告与发行归档。性能表来自报告当天的采样,写作时没有重新跑基准;未来方向采用同日官网路线图的 1.x、2.x、3.x 分组,均不代表已交付能力或日历承诺。

从 v1.7.0 优化链路,到 v1.7.1 资源与提交边界,再到后续内核演进

v1.7.0 留下的基础,v1.7.1 接住的问题

v1.7.0 已经交付结构化 HIR、六个静态优化 pass、有界整数 MIR、类型反馈 guard/deopt,以及局部整数 List 的标量替换。这条路径让我可以把一部分重复工作前移到编译阶段,再由独立 Verifier 检查。

但交付优化器,并不意味着所有应用已经默认变快。直接 Bytecode 仍是默认路径,AST 保留为独立语义对照;HIR、反馈特化和标量替换继续显式开启。标量替换也保留原有 GC 与配额分配预约,不能把它写成已经消除了堆分配。

到了 v1.7.1,我把注意力放回更接近应用的一层。语言现在有文件、HTTP、进程、Stream、常驻 Web 和数据库扩展。一次函数调用可以借出游标,一条惰性流水线可以把闭包环境保留很久,一次父任务失败也可能留下已经完成的外部写入。

如果 Runtime 不能准确回答这些对象的生存期,继续叠加快路径只会让错误更难解释。

先把资源的归属补牢

这次修复里,Context environment 的 GC roots 很关键。长期宿主调用依赖的环境需要显式保活,不能因为一次垃圾回收变成悬空引用。候选状态、事务与严格作用域也需要参与 roots 管理,并在异常出口清理。

Stream close 的变化同样具体:释放 payload、环境、回调和 jobs 引用,让已经结束的消费不再拖住这些对象。但为了保护检查点,注册链节点仍然保留。我不会把它概括成“关闭后所有资源节点立即删除”,因为那会掩盖真实的生命周期设计。

任务子进程的失败恢复,以及 portable no-replace 的修复,也都在处理同一种责任:正常路径建立了什么,失败路径就必须知道如何收尾。

这里还要分清内存口径。max_memory 限制的是相对 Runtime 启动基线的语言堆,不是整个进程 RSS,也不是所有 Worker 的内存总和。语言配额是可控运行的一部分;要判断常驻服务是否稳定,还需要持续观察 RSS、FD、子进程和临时文件。

七个实验接口,围绕四种明确的责任

v1.7.1 新增的七个受控并发 API 默认关闭,需要显式启用:

HHY_CONCURRENCY_EXPERIMENTS=1 hhy run app.hhy

它们分别解决 Runtime 内状态发布、单文件协作更新、数据库严格事务和有限批次任务监督。

范围接口我希望它承担的责任
单 owner 状态atomic_stateatomic_readatomic_updateatomic_close候选值校验完成后,一次发布完整状态
单文件atomic_file_update遵守同一锁协议的写者,在锁内完成读改写
数据库transaction_strict回调受约束地使用同一个事务,失败后不能继续提交
有界任务task_map隔离子进程、有序结果、失败监督与直接任务回收

这七个接口并没有把 HHY 变成共享内存并发语言。当前并发仍建立在同步 Runtime、隔离进程、有界并行与外部存储事务上,没有新协程调度器,也没有 async/await

我选择先把每种资源的保证做窄、做实。原子性需要回答“对什么原子”,才有讨论价值。

原子状态:把相关字段作为一个候选值发布

例如,我希望两个字段一起变化,而不是让读取者看到中间状态:

let state = atomic_state({left: 100, right: 0})
fn transfer(old) { {left: old.left - 10, right: old.right + 10} }
atomic_update(state, transfer)
print(atomic_read(state))
atomic_close(state)

atomic_update 同步计算并校验候选值,然后一次发布。提交前发生错误、取消或配额失败,旧值仍然保留。

这里的 owner 是所属 Runtime/PID。AtomicState 不能跨 Worker 发送,不能捕获到另一个 Worker 里,也不能 JSON 编码。它不是普通变量自动获得了线程安全,更不是跨进程共享内存已经完成。

候选值也有明确范围:Null、Bool、Int、有限 Float、String,以及递归 List/Map;Function、Stream 和句柄不能混进来。限制这些值,才能约束发布、生命周期和失败行为。

对我来说,这提供了一个足够小、可以验证的提交内核。以后扩展到多个对象、多个 Worker 时,需要新的内存模型和恢复协议,不能直接扩大这组 API 的承诺。

文件、数据库和任务,不能拼成一个自动事务

三个独立原子域及有界任务监督的保证边界

单文件更新最容易产生误解。原子替换解决可见性;多个写者先读再写是否丢更新,还取决于它们是否遵守同一套协作锁协议。

atomic_file_update 要求目标是预先存在的普通文件,所有写者围绕同一路径和稳定的 .hhy-lock 文件协作。传入的 Duration 只限制等待锁的时间,不会中断任意长的回调。活跃写者之间也不能随手删除锁文件,否则锁的身份就变了。

发布与持久化仍是两件事。临时文件写入和同步、替换、父目录同步,各自都有失败位置。HHY_PUBLISHED_DURABILITY_UNKNOWN 表示内容已经发布,但目录持久性未知;此时把错误理解成“完全没写入”,再盲目重试,就可能重复执行业务动作。

数据库也有对应的问题。transaction_strict 约束回调使用同一事务;回调运行错误即使被 catch,仍保持 rollback-only,不能通过吞掉错误恢复提交资格。严格回调也不能任意执行文件、HTTP、任务等外部效果。

但数据库 COMMIT 的应答丢失后,客户端可能只知道 DB_COMMIT_UNKNOWN。数据库已经提交、客户端却没有收到确认,是必须单独处理的状态。业务应该结合唯一请求键查询、对账,再决定下一步;语言没有自动提供 exactly-once。

task_map 则负责有限 List 输入、有界隔离任务和有序 List 结果。后面的任务快速失败,不必一直等第一个慢任务才能被发现;退出时会统一终止和回收直接子任务。不过,子任务已经完成的文件、HTTP 或数据库效果,不会随父任务失败自动撤销。它的结果预算也不是全局磁盘或 RSS 上限。

我希望开发者能够在调用之前知道,哪一部分会被一起提交,哪一部分需要自己恢复。 Runtime 状态、单文件和数据库是三个独立原子域,任务监督负责生命周期;它们不会自动组合成跨资源事务。

这次基线,让我重新看待“默认更快”

9 月 14 日的报告使用当前 1.7.1 二进制,对三个既有基准分别运行 AST 与 Bytecode。每种模式预热两次、正式采样七次,交替执行顺序,清除环境中的 HHY_* 实验开关,并检查每次输出一致。

每个样本都单独启动进程,时间包括启动、解析、编译或验证与执行。这测的是端到端成本,不能直接当成热循环的稳态速度。

HHY 1.7.1 同一二进制的双引擎耗时比:Flow 更快,JSON 接近,闭包更慢

负载AST 中位数Bytecode 中位数BC / AST
core-flow,100,000 项20.922 ms13.499 ms0.645×
json-flow,500 轮转换62.382 ms63.469 ms1.017×
call-closure,100,000 次捕获闭包调用21.985 ms28.518 ms1.297×

Flow 负载里,Bytecode 耗时少约 35.5%;JSON 接近;捕获闭包里,Bytecode 耗时多约 29.7%。三组输出对等,但速度方向并不一致。

我愿意把闭包这一行原样留下。下一轮真正值得做的,是通过 profiler、dispatch 和 lookup 对照,把启动成本与稳态成本分开,找到可以解释的热点。单凭这张表,还不能断定是哪一次查找或哪种派发导致了差异。

这组数据也不是 v1.7.0 与 v1.7.1 的版本对比。它比较的是同一 1.7.1 二进制的两个引擎。开发机没有锁频、绑核或停掉后台服务,也没有多主机重复;它适合做后续调查的基线,不能给出 p99、生产 SLA 或与其他语言的全面排名。

开发期 binding cache 与 source fusion 的配对记录同样呈现混合结果:部分负载略有收益,文件 distinct 等负载退化。那组记录使用的是开发前保存的对照二进制,并非从 tag 洁净重建,也不能拿绝对时间与本次表格相除,拼出一条版本提速曲线。

因此,编译器、cache 和 fusion 候选继续按真实负载收益、兼容与资源预算独立准入。代码存在,不构成默认开启的理由。

有验收证据,也要留下尚未回答的问题

这份报告新执行了 69 个引擎与配置用例,以及 80 次协作文件竞争更新。69 个用例包含预期拒绝,含义是行为符合契约,不是所有输入都应成功。

已有归档还包含双数据库幂等与提交未知恢复、100,000 请求的单机 Web 测试、四平台 CI 和发行包验收。Web 记录为 32 客户端、0 失败、约 2,391 req/s;这是当时单机单负载的观察值,不是最大承载量,更不是数据库业务 QPS。

120 秒、9,360,000 次 owner 提交的短时长稳,也属于旧构建的开发期证据。它不是当前发布二进制的 24 小时长稳,更不是共享内存并发测试。Linux x86_64 的极低内存 unwind 还存在 BDWGC/ASan 环境限制,不能把各平台 sanitizer 的覆盖范围写成完全相同。

发行过程中也出现过性能门槛波动:首次 macOS I/O/JSON 比值为 1.3496,超过 1.10 门槛;同提交本地复核为 1.0032,在不改代码和阈值的情况下重跑失败 jobs 后通过。记录这段过程,比把它压缩成“首轮全部通过”更有助于以后理解测量环境。

接下来,我优先补当前候选构建的 24 小时长稳、提交前后故障矩阵,以及数据库隔离级别、死锁、重复请求和重启恢复的业务不变量。然后再做 Web 并发阶梯、尾延迟和资源曲线。测试数量可以增长,承诺范围仍应跟着证据走。

1.x:让已有能力逐项稳定开放

官网路线图把接下来的工作分成三个大版本系列。我会用这个层级表达方向,而不把内部拆分草案中的每个小版本号写成发布日期。

1.x 的重点是稳定当前七个接口:先固定 owner、effect、错误分类与资源契约,再用当前候选构建完成长稳、故障、平台和 AST/Bytecode 对等验收。

状态、文件、数据库和任务应分别判断。某一项成熟,可以先移除它的实验门槛;没有满足条件的继续保留实验状态,不需要彼此捆绑。

“稳定开放”意味着不再要求 HHY_CONCURRENCY_EXPERIMENTS 才能调用已经准入的 API。开发者仍然需要显式调用原子更新或严格事务,普通变量不会因为升级突然变成事务变量。

性能优化是另一条准入线。HIR、cache 或 fusion 即使正确性通过,也仍需证明端到端收益,可以长期保持关闭。

2.x:共享状态与协程,需要一起设计内核

HHY 后续路线图:1.x 稳定化,2.x 原子共享状态与协程,3.x 分布式事务

2.x 的目标,是域内全局状态原子性、跨 Worker 的共享内存原子状态,以及协程调度器。

这里的“全局”有范围:一个声明的 StateDomain 内,受管共享对象可以参与统一的多对象事务。它不代表任意普通变量、文件、HTTP 和数据库会被自动包成一个事务。

共享内存也不能直接传递当前 GC 堆里的指针。它需要共享 arena、带代际的句柄、版本与读写集、统一提交点,以及 Worker 崩溃后的恢复和回收规则。同机 Worker 共享与跨节点分布式一致性,是不同层次的问题。

协程则要求可挂起和恢复的执行帧,保留局部值、异常处理器、资源作用域与 GC roots,再接入非阻塞 I/O 和结构化取消。已有 Stream 或 SSE 并不等于这些工作已经完成。

我尤其关注共享事务和协程相遇的地方:原子提交过程中能不能挂起,取消到达时由谁清理候选,等待的任务是否会丢失唤醒,旧版本由谁保留。两部分分别通过测试之后,还需要联合验证。

新模式应该先实验,满足正确性、恢复、公平性与资源边界后再稳定。旧程序不能在没有迁移选择的情况下,突然改变共享或调度语义。

3.x:把未知结果带进可恢复的分布式协议

3.x 才进一步走向分布式事务:持久协调器、prepare/commit/rollback 参与者、决策日志、UNKNOWN 查询与恢复,再到高可用、故障注入和运维工具。

我希望支持的是明确具备事务协议能力的参与者。普通 HTTP 服务如果不支持可恢复的准备和提交,就不能因为接入 HHY 而自动获得严格原子回滚;显式补偿有价值,但补偿与原子回滚需要分别说明。

这条路线的验收重点,会是参与者崩溃、网络分区、重复消息、确认丢失,以及协调器重启后如何继续执行已经记录的决定。稳定的 API 也仍然需要显式配置数据库、协调器和支持矩阵。

从 v1.7.0 到 v1.7.1,我对 HHY 的期待没有变:让数据流清楚,让系统行为可解释。只是现在,每增加一种能力,我都会多问一句:它失败以后,使用者还能不能知道发生了什么,并且继续把事情做完。

参考资料与延伸阅读

返回技术札记