CD Code Digest Hub /GTA V

字节码、代码生成、链接与优化

7 分钟阅读 8 个章节

1. 栈式指令集#

opcodes.h 是指令集合与长度等元数据的集中来源;opcode.h 定义枚举。编译器有 OP_FIRST_INVALID == 131 的编译期断言。这里的 131 是当前编号空间边界,不是一次游戏运行实际使用的指令数。[S1]

指令主要包括:

组例子工作方式
数值IADD/FADD/VADD、比较、I2F/F2I按整数、浮点、向量解释栈顶
常量与栈PUSH_CONST、DUP、DROP不同宽度立即数压栈
存储LOCAL/STATIC/GLOBAL、LOAD/STORE、LOAD_N/STORE_N通过帧/实例/共享区域偏移访问
聚合寻址ARRAY、IOFFSET数组下标检查、结构字段地址计算
流程J、JZ、SWITCH分支和循环
脚本调用CALL、CALLINDIRECT、ENTER、LEAVE保存返回地址、帧指针和返回值
原生调用NATIVE按程序 Native 表索引找到包装器
文本/特殊STRING、TEXT_LABEL_*、CATCH、THROW字符串池、缓冲区及非局部恢复

普通变量没有运行时名字查找;名字主要用于编译期和调试。与寄存器 VM 相比,栈式 VM 的生成器简单,但算术组合可能产生更多栈操作和解释器派发。

2. 从源语句到 opcode#

以下是讲解用的逻辑序列,不是本次执行编译器获得的反汇编:

text
源:score = score + 1

朴素逻辑:
    取得 score 地址
    LOAD
    PUSH_CONST 1
    IADD
    取得 score 地址
    STORE

可能压缩为:
    LOCAL_U8_LOAD <slot>
    IADD_U8 1
    LOCAL_U8_STORE <slot>

具体指令取决于 score 是 local/static/global、偏移范围、邻接指令和分页边界。不能把教学序列当成固定真实输出。

语句 AST 直接调用 Emit。IF/循环先留出跳转位置,发射主体后回填;函数调用先记录尚未发射的目标,再由 EmitAccumulatedCalls 生成函数并修补 24 位地址。[S2]

3. 已确认的优化#

优化实现证据适用范围与限制
常量折叠scrNodeBinary::IsConstant主要是已支持的整数常量操作;不是通用执行器
常量分支消除scrNodeIf::Emit常量条件选择分支
按引用生成函数EmitAccumulatedCalls从调用/取地址出发补发函数体
紧凑立即数EmitInt、EmitFloat小值专用 opcode、U8/S16/U24/U32 等
地址+读写融合EmitOpLOCAL/STATIC/GLOBAL/ARRAY/IOFFSET 后接 LOAD/STORE
算术立即数融合EmitOpPUSH 常量 + IADD/IMUL/IOFFSET
比较跳转融合EmitOp整数比较 + JZ
多常量压栈融合EmitOp连续 U8 常量压栈合并
字符串池复用scrNodeString::Emit 附近查找已有字符串,避免重复存储
热 Native 内建化EmitNativeStubIS_BIT_SET 特判为 opcode
发布裁剪DEBUGONLY、-final去除相关调用/名字,不是业务语义无关的任意优化

这些是树级简化、按需生成与窥孔优化,未发现当前主链路使用 SSA、通用循环向量化、JIT、动态去虚拟化或成熟的全程序别名分析。不要将 C++ 输出路径中的历史注释当成当前 VM 已取得的性能收益。[S2][S3]

4. 分页同时约束指令布局#

程序代码和字符串页大小为 16 KiB。EmitOp 明确避免普通指令跨越代码页;不够放时填 NOP 或生成跨页跳转。跳转、字符串池偏移和解释器 PC 换页必须协同处理。[S2][S4]

这解释了为何同样指令级优化在页尾可能不能应用:优化不仅改变速度,还改变指令长度和跳转距离。短跳转、24 位 CALL、11 MiB 编译缓冲和页内连续要求是不同维度的约束。

分页有利于资源系统装载和内存组织,但“分页资源”不等于操作系统按需缺页或任意指令执行时自动请求磁盘。当前代码支持的具体流式策略需看资源系统的装载生命周期。

5. 调用格式和 ABI 成本#

ENTER 编码参数槽数、局部帧大小和可选过程名;参数槽数用 u8,局部数用 u16。大结构体按值传参可能在达到 256 槽前触发参数限制,而不是按“有几个形参”计算。[S3]

NATIVE 长度为四字节:opcode、包含返回槽数与参数槽数的打包字节、两字节 Native 表索引。返回槽数用两位,参数槽数用六位,当前编译器 Native 表容量为 4,096。[S3] VECTOR 返回需要三槽;任意大 STRUCT 返回不在这套 Native 返回编码的直接容量内。

这也是改语言时必须升级工具链而非只改 parser 的原因:类型大小、指令编码、包装模板、资源格式和调试器都依赖同一 ABI。

6. 产物不是只有字节码#

scrProgram 包含代码页、静态初值、全局初值/块信息、字符串页、Native 标识数组、启动参数大小和名称/调试相关数据。保存 .sco 时还处理压缩、字节序与 Native 表重排及引用重映射。[S5]

Native 名称并非每次调用重新哈希查找。装载验证时将标识解析成函数指针,运行时 NATIVE 访问程序表中该槽位。这一提前绑定降低了频繁跨边界调用的查找开销,但要求程序与命令注册表匹配。

7. 优化正确性与新语言借鉴#

新增优化要验证“结果、错误、挂起点、调试映射、副作用顺序”都保持一致。特别是常量折叠的除零规则与 VM 路径存在静态可见差异;这种问题应以差分测试解决,不能仅测试正常算术。

适合继承的是:小型 typed IR、可解释的优化、紧凑常量编码、Native 提前绑定、保留源映射。建议把指令定义作为单一 schema,自动生成解码、校验、反汇编和文档;让优化前后能在同一小型参考解释器上比较。

源码证据#

  • S1 — opcodes.h;sc.cpp 编号断言。
  • S2 — EmitOp 窥孔和分页;调用回填。
  • S3 — ENTER 生成;NATIVE 生成;表容量。
  • S4 — program.h 分页与程序上限。
  • S5 — program.cpp::Save。