性能成本模型、瓶颈与优化方法
1. 本报告能够与不能够给出的结论#
本次没有编译、运行、抓帧或抓包,因此没有 SanScript 的实测 ns/op、帧耗时、峰值内存或带宽曲线。以下内容是基于源码机制的成本分析和可执行测量方案,不宣称它比 Lua、C#、JavaScript 或其他 VM 快多少。
源码行数、脚本文件数、opcode 数、Native 次数和真实 CPU 时间是不同指标。应把离线编译成本、运行 CPU、实例内存、资源创建和网络成本分别测量,再关联到同一玩法场景。
2. 一帧中的成本分解#
T_scripts ≈ Σ实例 [ T_dispatch + T_value/stack + T_script_calls
+ T_native_marshalling + T_native_body ]
+ T_scheduler + T_debug/profiling
后续引擎工作 = AI/物理/渲染/流式加载/网络等被脚本触发的成本这里是分析模型,不是可直接相加的全部墙钟测量:异步任务、并行系统和嵌套 profiler 会导致重复计算或时间重叠。应明确计时范围及 inclusive/exclusive 定义。[S1]
3. VM 层的成本与已有减负#
栈式表达式需要取/存槽、解释器派发和临时值移动。当前实现用局部 SP/FP、紧凑指令、立即数 opcode、读写融合、比较跳转融合以及部分平台 computed goto 减少这些成本。[S2]
按值传大 STRUCT/数组会复制多个槽,ENTER 还要初始化局部。引用可以降低复制,但增加别名、寿命和写回约束;不能无差别把所有参数改成引用。STRINGHASH 的字面量在编译期转换,变量字符串可能每次运行哈希。
SWITCH 的快速内核遍历 case 表匹配,不是普遍保证 O(1) 的跳转表。[S3] 大型、每帧反复执行的 SWITCH 值得测量;但小状态机可读性很好,不应凭实现线性扫描就全部改写。
4. 高频 Native 比语法复杂度更值得先看#
一个简单的 getter 仍有包装、句柄查找、条件检查等成本;复杂 Native 可能执行空间查询、分配或创建网络实体。循环条件和循环上界如果调用 Native,可能在每轮迭代反复执行。
优化顺序通常是:减少不需要的更新 → 降低频率 → 缓存本帧稳定结果 → 批处理必要查询 → 再优化 opcode。缓存必须定义失效点,尤其跨 WAIT、模型卸载、对象死亡和主机迁移后不能继续相信旧结果。[S4]
生产 main.sc 已把维护分成每帧、一秒和五秒三个频率层。[S5] 这是从真实代码可确认的策略,比“所有控制器每帧全量更新”更节省预算。
5. 内存放大#
程序代码可以共享,而实例静态、帧和栈每条线程独立。头文件中一个大型静态数组若被多个同时存在的脚本包含,可能按实例重复占用。64 位槽还会放大基础槽存储;但整数有效宽度仍是 32 位,不能把空间增加视为数值能力增加。[S6]
建议同时观察:代码/字符串页字节、静态槽数、每实例预留栈、最大已用栈、全局块占用、活跃实例数、Native 资源数量。仅看脚本系统分配器的总量,可能无法解释是程序资源增加还是实例增加。
6. 调度性能与正确性相互约束#
WAIT 让出执行但并不消除每帧调度和等待检查成本。大量长期空闲线程可能更适合事件唤醒或定时器队列;不过现有设计以简单遍历提供稳定可理解的顺序。
不要依赖开发内核预算替代主动挂起。快速内核的最终配置不继承同样的外部指令上限,且 Native 开销无法由 VM 指令计数精确控制。[S7]
把重任务分成多帧可降低尖峰,但会改变外部可见时序和取消窗口;对注册存档结构等要求同帧完成的 API,不能任意插入 WAIT。优化必须遵守引擎阶段契约。
7. 离线编译和生产效率#
当前源码体量超过一千万物理行,最大抽样文件 carmod_shop_core.sch 为 88,463 行,freemode.sc 为 55,439 行。这些是大小指标,不是耗时证明;巨型表和头文件依赖可能放大 AST、符号、字符串以及增量重建成本。
工程配置把编译器 node heap 提升到近 GB,且提供头文件静态变量报告、未引用结构字段/全局引用工具,说明内容规模下的可见性很重要。[S8] 新语言可优先提供编译时间分项、模块缓存、依赖图和只读数据外置,而不是只在 VM 上追求小幅指令节省。
8. 建议的测量矩阵(未来工作,本次未执行)#
| 场景 | 控制变量 | 观察指标 |
|---|---|---|
| 纯算术/分支 | 相同算法、相同输入,不调用 Native | 每次迭代耗时、opcode、分支行为 |
| 函数与聚合 | 值/引用、结构大小、调用深度 | 复制量、栈峰值、调用开销 |
| Native 边界 | 空包装、小 getter、批量接口 | 封送与函数体分别计时 |
| 调度 | 活跃/等待实例比例、等待长度 | 调度时间、唤醒延迟、尾部帧时 |
| 资源创建 | 模型热/冷、批量创建、清理 | 流式等待、分配、后续引擎成本 |
| 多人同步 | 字段变化率、范围、迁移与丢包 | 字节、重发、CPU、恢复时间 |
| 编译增量 | 改入口/公共头/类型/选项 | 重编译文件数、耗时、峰值内存 |
报告应记录平台、构建宏、代码哈希、数据集、运行次数、预热和统计口径,使用中位数与尾部延迟,不只给平均值。先确定瓶颈,再改一个因素复测。
9. 给新语言的优先级#
第一优先级是可观测性与可限制的副作用:能知道哪个脚本/Native 花时间、创建多少资源、占多少栈、发多少数据。第二是数据布局、模块边界与更新频率。只有确认派发成本占主导时,才评估寄存器 VM、更多 superinstruction、AOT/JIT。
对小型团队,带验证器的紧凑字节码 VM、良好的 FFI 与 profiler 往往比复杂优化后端更快形成可用产品;这是工程建议,不是对所有工作负载的性能排名。
源码证据#
- S1 — Native 与脚本分开计时。
- S2 — 窥孔优化;快速内核。
- S3 — SWITCH 匹配循环。
- S4 — FOR / REPEAT 重算上界。
- S5 — main.sc 分频维护。
- S6 — scrValue 槽位;实例静态复制。
- S7 — 预算路径差异。
- S8 — 编译器内存选项;静态变量报告工具。