本章导读
性能讨论最怕两件事:凭空猜测和过度泛化。本章的方法论与第 40 章一脉相承——用 cgen 产物作证。每讲一个内存行为,都给出对应的生成 C 代码片段;每下一个性能结论,都标明它来自哪一层机制。xlang 的性能故事可以用一句话概括:标量零开销,引用一次计数,装箱只发生在需要动态类型的边界。没有 GC 停顿,没有隐藏堆分配,没有运行时类型探测——代价是你必须理解引用语义,收益是行为完全可预测。本章读完后,你面对任何 xlang 代码都能回答三个问题:这个值存在哪里?谁负责释放它?热路径上有几次原子操作?
掌握 xlang 值存储的两大分类(标量直存 vs xvalue 装箱)及各自的 C 表示;理解 xvalue 24 字节固定外壳的位域布局(Type 4bit / IsStatic 1bit / RefCount 26bit);掌握原子引用计数的工作原理与确定性清理机制(无 GC、作用域边界生成 cleanup);理解静态泛型容器(xtarray/xtlist/xtset/xtdict)与动态容器(xvalue)的性能差异及选型依据;掌握字符串不可变语义与 xrtTempScope 临时内存作用域;理解 class 对象的堆分配、payload 布局与析构顺序;了解闭包环境的原子引用管理;理解 virtual 派发与直接调用的开销差异;学会使用内存遥测工具验证程序的内存行为。
41.1 性能哲学:生成 C 的性能基线
第 40 章证明了 xlang 编译的最终产物是 C 代码,由 TCC 编译为机器码。这个架构决定了一条根本性的性能等式:
xlang 程序的性能 ≤ 等价手写 C 程序的性能——等号在标量运算、直接调用、静态容器操作时成立;差距来自 xvalue 装箱、引用计数原子操作和虚表间接跳转。
这个等式有三层含义:
一,标量运算是原生速度。int 加法生成 long long 加法,float 乘法生成 double 乘法——C 编译器(TCC 或系统 cc)看到的就是最平凡的机器指令,没有任何中间层。第 3 章的 int/float/bool 在 cgen 后与 C 的 long long/double/bool 完全等价。
二,TCC 不做激进优化。TCC 的设计目标是编译速度而非代码优化——它不内联、不向量化、不做循环展开。这意味着热循环里的标量运算虽然语义上是原生 C,但可能比 GCC -O2 慢 2–5 倍。这是设计权衡:xl run 的毫秒级启动和 xl.dll 的即时脚本执行,换来的代价是峰值吞吐。需要极致性能时,xl build 产物可交系统编译器二次优化(第 37 章 --cc 选项)。
三,开销可枚举。xlang 相对纯 C 的额外开销只有有限几种:xvalue 堆分配(动态容器/var/class 句柄)、原子引用计数(retain/release)、虚表间接跳转(virtual 方法)、类型描述符查询(is/as)。没有 GC 扫描、没有闭包逃逸分析、没有运行时泛型擦除——因为泛型在编译期单态化(第 14 章),重载在语义阶段定版(第 40 章)。
41.2 值存储分类:标量直存与 xvalue 装箱
xlang 的每一个值在生成 C 里只有两种存储形态:
| 存储形态 | C 表示 | 适用类型 | 分配位置 | 释放方式 |
|---|---|---|---|---|
| 标量直存 | long long / double / bool / const char* / void* / xtime / 生成的 C union | int, float, bool, string, point<T>, time, type, union | 栈(局部变量)或 payload 内(class 字段) | 作用域结束自动失效,无需释放 |
| xvalue 装箱 | xvalue(指向 xvalue_struct 的指针) | class 句柄, 动态容器, var, function 值, 闭包 | 堆(xrtMalloc) | 引用计数归零时释放 |
用 cgen 取证验证——同一段代码里两种存储并存:
func demo() int count = 42 // 标量直存 float ratio = 3.14 // 标量直存 string name = "xlang" // 标量直存(const char*) auto nums = [1, 2, 3] // 静态泛型 → xtarray(堆,引用计数) var anything = "hello" // xvalue 装箱(堆) end
// cgen 产物(关键行): int64_t count = 42LL; double ratio = 3.14; const char* name = "xlang"; xtarray nums = xrtTypedArrayCreate(xrtTypeInt(), XRT_OBJMODE_LOCAL); // ... xrtTypedArrayAppend(nums, &(int64_t){1LL}) × 3 ... xvalue anything = xvoCreateString("hello", 5);
前三个变量是栈上的平凡 C 值——赋值就是 mov,比较就是 cmp,函数返回后栈帧回收,零额外开销。nums 是堆上的类型化数组,创建时一次 xrtMalloc,函数退出时 cgen 生成的 cleanup 调用 xrtTypedArrayRelease。anything 是堆上的 xvalue 外壳,退出时 xvoUnref 释放。
关键认知:类型声明决定存储形态,不是初始值。int x = someVar 里 x 永远是栈上 long long;var x = 42 里 x 永远是堆上 xvalue——即使装的是同一个整数 42。这就是第 15 章"var 是动态类型的终极形态"在内存层面的含义。
41.3 xvalue 结构解剖
xvalue 是 xlang 动态世界的统一货币——class 句柄、动态容器、var、function 值在 C 层都是 xvalue_struct*。这个结构体大小固定,设计文档明确要求"不向值外壳追加可变字段":
struct xvalue_struct { union { struct { uint32 Type:4; // 数据类型标签(0–15) uint32 Reserve:1; // 保留 uint32 IsStatic:1; // 静态值标记(null/true/false) uint32 RefCount:26; // 引用计数(最大 67,108,863) }; volatile uint32 Header; // 原子 CAS 操作的目标 }; uint32 Size; // 数据大小 union { // 值体(8 字节) int64 vInt; double vFloat; str vText; xtime vTime; ptr vPoint; xparray vArray; xlist vList; xset vSet; xdict vTable; xrt_callable* vCallable; ptr vCustom; }; union { // 扩展(8 字节) xvalue_struct* vFuncEnv; ptr vExtra; const xrt_type_desc* vTypeDesc; }; };
在 64 位平台上,这个结构体恰好 24 字节(4 + 4 + 8 + 8)。三个设计要点:
一,Header 是原子操作单元。Type、IsStatic、RefCount 编码在同一个 32 位字里,引用计数的 retain/release 通过对 Header 的 CAS(Compare-And-Swap)完成——一次原子操作同时检查类型和递增计数,不需要额外的锁或独立的计数字段。26 位计数上限约 6700 万,对任何合理程序都足够。
二,值体是 union 而非指针。int/float/time 直接内联在 xvalue 里,不产生第二次堆分配。只有 string(vText 指向堆上字符数组)、容器(vArray 等指向容器结构)和 class(vCustom 指向 payload record)才需要额外的堆内存。这意味着 var x = 42 的开销是一次 24 字节的 xvalue 分配,而不是"分配外壳 + 分配整数"两次。
三,vTypeDesc 承载精确类型。class 对象通过 vTypeDesc 指向编译期生成的类型描述符——继承关系、虚表、字段布局、copy/clone 能力全部挂在这里。is/as 的类型检查(第 7 章)就是沿 vTypeDesc 的父类链查找,不需要字符串比较或哈希表。
41.4 引用计数与确定性清理
xlang 没有垃圾回收器。每一个堆对象的生命周期由引用计数精确管理,清理代码由 cgen 在编译期生成——这是"确定性清理"模型:
赋值、参数传递、返回、闭包捕获、容器存储 → xvoAddRef(引用 +1)
变量离开作用域、被重新赋值、容器元素被移除 → xvoUnref(引用 −1)
引用归零 → 执行析构(class 的 destroy)→ 释放 payload → 释放 xvalue 外壳
cgen 为每个需要清理的变量在作用域末尾生成显式释放代码。用 cgen 取证观察:
class Box int value create(int v) self.value = v end destroy() println("Box destroyed") end end func demo() Box b = Box(1) println(b.value) end
// cgen 产物(demo 函数体,关键行): xvalue b = xvoCreateObject(&xl6_td_Box); xl6_td_Box.create(b, 1LL); xl6_xrt_println(...); // ---- 作用域结束,cgen 生成清理 ---- xvoUnref(b); // 引用归零 → 触发 destroy → 释放 payload → 释放外壳
清理的确定性意味着:你精确知道对象在哪一行死亡。没有 GC 的"某个不确定时刻回收",没有 finalizer 的执行顺序问题。Box 的 destroy 在 demo 函数的最后一条 xvoUnref 时恰好执行一次——不多不少。
原子性保证。xvoAddRef 和 xvoUnref 内部使用 CAS 原子操作(xrtAtomicCompareExchangeU32),因此同一个对象可以安全地被多个线程/task 同时持有和释放。但注意:原子引用计数只保证句柄生命周期的线程安全,不保证对象成员读写的线程安全——共享可变状态仍需 mutex/rwlock(第 28 章)。
重新赋值即释放旧值。b = Box(2) 生成:先 xvoUnref(旧 b),再 xvoCreateObject 创建新对象。如果旧 b 是最后一个引用,析构立即发生——不会等到函数结束。这个行为与 Python 的引用计数类似,但 xlang 的清理点由编译器静态确定,不依赖字节码解释器的引用计数指令。
41.5 静态泛型容器 vs 动态容器
第 16–17 章介绍了 array/list/set/dict 四种容器。第 40 章揭示了它们在 cgen 层的分裂:同质字面量产生静态泛型容器(xtarray/xtlist/xtset/xtdict),异质字面量或 var 上下文产生动态容器(xvalue 包装的通用容器)。这个分裂的性能含义巨大:
| 维度 | 静态泛型容器(xtarray 等) | 动态容器(xvalue) |
|---|---|---|
| 元素存储 | 直接存储 C 值(int64_t / double / xvalue 句柄) | 每个元素是 xvalue 指针,指向堆上 24 字节外壳 |
| 装箱开销 | 标量元素零装箱 | 每个标量元素一次 xvalue 分配 |
| 遍历开销 | 连续内存,缓存友好 | 指针追踪,缓存不友好 |
| 类型检查 | 编译期确定元素类型,运行期零检查 | 每次取值可能需要运行时类型判断 |
| 引用计数 | 容器整体一次 retain/release | 容器 + 每个元素各自 retain/release |
| C API | xrtTypedArrayGet/Set(直取) | xvoArrayItemGet/Set(通用接口) |
实测对比——同样存储 1000 个整数:
// 静态泛型:元素是裸 int64_t,连续内存 auto typed = [0] * 1000 // array<int> → xtarray // 动态容器:每个元素是 xvalue 指针 → 堆上 24 字节外壳 var dynamic = [0] * 1000 // 动态 array → xvalue
// cgen 产物对比: // typed 的创建: xtarray typed = xrtTypedArrayCreate(xrtTypeInt(), XRT_OBJMODE_LOCAL); // 追加元素:直接写 int64_t,无装箱 xrtTypedArrayAppend(typed, &(int64_t){0LL}); // dynamic 的创建: xvalue dynamic = xvoCreateArray(); // 追加元素:先创建 xvalue 外壳,再挂到容器 xvoArrayItemAdd(dynamic, xvoCreateInt(0));
选型规则:如果元素类型在编译期已知且同质,永远优先让编译器推断为静态泛型容器。只有确实需要混合类型([1, "hello", 3.14])或需要 var 多态时,才使用动态容器。第 40 章的容器推断规则(同质→静态,异质→动态)不是语法糖——它是性能分界线。
41.6 字符串与临时内存
xlang 的 string 在 C 层是 const char*——不可变的 UTF-8 字节序列。这个设计带来两个性能特性:
一,字面量零分配。字符串字面量 "hello" 在生成 C 里就是 C 字符串字面量,存储在只读数据段,程序生命周期内不分配不释放。只有字符串运算(拼接、切片、格式化)才产生新的堆分配。
二,拼接产生新对象。a + b 不修改 a 或 b,而是分配一块新内存存放结果。连续拼接 a + b + c + d 会产生三次中间分配——与 Java/C# 的不可变字符串行为一致。热路径上的大量拼接应使用 f-string(第 19 章)一次成形,或借助标准库的 StringBuilder。
临时内存作用域(xrtTempScope)是 XRT 为字符串运算提供的 arena 分配器。cgen 在表达式语句开头插入 xrtTempScopeBegin(),在语句结束后调用 xrtTempScopeEnd() 一次性回收该语句产生的所有临时字符串——避免逐条 xrtFree 的碎片和开销:
// cgen 产物(含临时字符串的表达式): xrtTempScope __xl6_temp_scope_0 = xrtTempScopeBegin(); const char* msg = xrtFormat("value = %d", count); xl6_xrt_println(msg); xrtTempScopeEnd(__xl6_temp_scope_0); // 一次回收所有临时分配
临时内存是线程本地的(thread-local),不需要锁,分配是简单的指针前移(bump allocator),释放是整块回收。对于短生命周期的字符串运算,这比通用 malloc/free 快一个数量级。
41.7 对象分配与 payload 布局
class 对象是 xlang 里最"重"的值——创建时涉及一次堆分配和类型描述符绑定。cgen 调用 xvoCreateObject(&xl6_td_ClassName) 完成创建,内部分三步:
① 分配 record。一次 xrtMalloc,大小 = xvalue 外壳(24 字节)+ payload 结构体大小。payload 的布局由编译期生成的 C struct 决定:
// class Point { int x; int y } 的 payload: typedef struct XL_Class_Point { int64_t x; int64_t y; } XL_Class_Point; // 总分配 = 24(外壳)+ 16(payload)= 40 字节,一次 xrtMalloc
② 初始化 Header。Type 设为 XVO_DT_CLASS,RefCount 初始为 1,vTypeDesc 指向编译期生成的类型描述符(含父类链、虚表、字段偏移、copy/clone 能力标志)。
③ 零初始化 payload。所有字段归零——int 为 0,class 句柄为 null,容器为空。然后调用 create 构造器(如果存在)。
继承布局:子类 payload 的第一个成员是父类 payload(第 22 章),C 层可以直接把子类指针当父类指针用——向上转换零开销,不复制、不分配、不调整指针。虚表指针挂在 type descriptor 上而非 payload 里,因此 payload 保持纯数据,C 互操作时可以直接 memcpy 字段(虽然 xlang 层面不鼓励这么做)。
析构顺序:引用归零时,先调用子类 destroy(),再逆序清理字段(class 字段 xvoUnref、容器字段 release),最后释放父类部分。整个析构链恰好执行一次——没有 GC finalizer 的"可能不执行"问题,也没有 C++ 虚析构的"忘记 virtual"陷阱。
41.8 闭包与函数值
第 13 章的闭包在 C 层由两部分组成:callable 结构(函数指针 + 签名元数据)和环境 record(捕获变量的快照)。环境 record 使用 XRT 原子引用计数管理——同一个闭包可以被多个 thread/task 同时持有,环境在最后一个引用释放时销毁:
// cgen 产物(闭包创建): xrt_callable* __xl6_fn_0 = xrtCallableCreate(xl6_lambda_0, env_record); // 捕获环境通过 xrtAtomicRefRetain/Release 管理 // 闭包赋值给变量时:xvoAddRef(外壳)+ xrtAtomicRefRetain(环境)
性能要点:
一,闭包创建是一次堆分配(环境 record)+ 一次 callable 分配。如果捕获变量全是标量,它们被值拷贝进环境——后续修改原变量不影响闭包(第 13 章的快照语义)。如果捕获的是 class/容器句柄,环境 retain 同一对象——修改可见。
二,闭包调用比直接函数调用多一次间接跳转(通过 callable 的函数指针)和一次环境指针传递。在热循环里,如果闭包不需要捕获,考虑改用普通函数或泛型函数(编译期内联可能性更高)。
三,闭包不能跨 async/generator 边界捕获 byref/out 参数(第 12 章限制)——这不是语法洁癖,而是因为 byref 绑定的是调用方栈上的变量槽,异步执行时栈帧可能已销毁。
41.9 虚派发与直接调用
xlang 的方法调用在 cgen 层只有两种形态:
| 调用形态 | 生成代码 | 开销 | 适用场景 |
|---|---|---|---|
| 直接调用 | xl6_ClassName_methodName(obj, args...) | 一次直接 call 指令 | 非 virtual 方法、static 方法、普通函数 |
| 虚派发 | obj→vTypeDesc→vtable[slot](obj, args...) | 一次内存读取(vtable)+ 一次间接 call | virtual/override 方法 |
重载不是虚派发。第 14 章的重载函数在语义阶段已经解析为具体符号(xl_fn_show__int),cgen 生成直接调用——运行期零分派开销。只有 virtual 关键字声明的方法才走虚表。
虚派发的真实成本:一次额外的内存读取(从 type descriptor 取 vtable 指针,再从 vtable 取函数指针)和一次间接跳转。在现代 CPU 上,如果分支预测器能预测目标(循环里对同一类型反复调用),间接跳转的惩罚接近零;如果目标类型高度多态(数组里混存几十种子类),每次调用都可能 miss 预测。
final 的性能意义:标记 final 的方法禁止后续 override,编译器可以将其降级为直接调用——即使它声明为 virtual。在性能敏感的热路径上,对确定不再被覆盖的方法使用 final 是零成本的优化。
41.10 内存遥测与诊断
XRT 内建内存遥测能力,可以在运行期追踪每一次分配和释放。启用方式(编译时宏):
# 在 xl build 或 xl run 时定义 XRT_MEM_DEBUG # 程序退出时自动生成 xrt_mem_report_auto.txt / .json
报告包含以下关键指标:
| 指标 | 含义 | 健康值 |
|---|---|---|
| live_alloc_count / bytes | 程序退出时仍存活的分配 | 0(所有分配已释放) |
| live_object_count | 仍存活的 xvalue 对象 | 0 |
| double_free_count | 重复释放次数 | 0 |
| overflow/underflow_count | 缓冲区越界写入 | 0 |
| temp_peak_bytes | 临时内存峰值使用 | 越低越好 |
| invalid_free_count | 释放非 XRT 分配的指针 | 0 |
一份健康的报告长这样(实测):
# XRT Memory Debug Report - status: clean - summary: no memory issues detected - live_alloc_count: 0 - live_alloc_bytes: 0 - live_object_count: 0 - double_free_count: 0 - overflow_count: 0
如果 live_alloc_count 不为 0,报告的 "Live Allocations" 段会列出每一处泄漏的分配点(文件名:行号)、大小和分配时间——精确到 XRT 库内部的哪一行代码做了那次 xrtMalloc。Site Stats 段按分配点聚合统计,帮助你定位热点分配路径。
实战建议:在 CI 或测试脚本里启用 XRT_MEM_DEBUG 运行全部测试用例,断言 live_alloc_count == 0。这是 xlang 标准库和编译器自身的验收标准——你的项目也可以采用同样的纪律。
深入细节
class 字段接收动态容器、静态泛型容器或其他 class 句柄时,会立即进入 XRT shared 存储(XRT_OBJMODE_SHARED)。shared 的含义是:引用计数使用原子操作,容器 backing 支持多线程同时持有。但 shared 不等于"线程安全"——多个线程同时写同一个 shared 容器仍需 mutex 保护。生成代码通过内部 xl6_xrt_move_to_shared 完成拥有权转移:原本地临时值在共享副本建立后立即释放。共享值图最多允许 256 层容器嵌套;循环引用以发布失败结束,不会无限递归。
xvalue 外壳统一使用 xrtMalloc/xrtFree,底层是 XRT 的线程本地 size-class 内存池(mempool_fs)。小对象(≤ 几百字节)从预分配的 slab 中按 size class 分配,不需要系统调用;大对象回退到系统 malloc。这意味着 xvalue 的分配/释放开销远低于通用 malloc——接近 arena 分配器的速度,但保留了逐个释放的能力(引用计数需要)。内存遥测报告里的 allocator=fsmempool 就是从 size-class 池分配的标记。
cgen 维护一个编译期清理栈(xl6_cgen_expr_cleanup.c):每声明一个需要清理的变量,就压入一条 CleanupEntry(变量名 + 清理类型)。作用域结束时,按 LIFO 顺序生成释放代码。清理类型包括:CLASS(xvoUnref)、NATIVE_CLASS(调用 extern destroy)、TYPED_CONTAINER(xrtTypedArrayRelease 等)、XVALUE(xvoUnref)、FUNCTION(callable release)、FUTURE/CHANNEL/THREAD/GENERATOR/SYNC(对应资源释放)、STRING(xrtFree)、TEMP_SCOPE(xrtTempScopeEnd)。这个栈保证了:无论函数有多少个 return 分支、多少层嵌套,每个变量都恰好被释放一次。
null、true、false 是编译期静态 xvalue(XVO_VALUE_NULL / XVO_VALUE_TRUE / XVO_VALUE_FALSE),IsStatic 位为 1,RefCount 为 0。xvoAddRef 和 xvoUnref 对静态值是空操作——它们永远不被分配也永远不被释放。这意味着 var x = null 不产生堆分配,bool 参数传递不产生引用计数开销。静态值是 xvalue 世界里唯一的"免费午餐"。
⚠️ 常见错误
var sum = 0 让 sum 成为堆上 xvalue,每次 sum = sum + i 都涉及 xvoUnref(旧值)+ xvoCreateInt(新值)——两次堆操作。改为 int sum = 0 后,sum 是栈上 long long,加法是一条 add 指令。规则:如果你知道类型,就声明类型。
for ... result = result + item 每次迭代分配一个新字符串并释放旧字符串——O(n²) 的总分配量。应使用 f-string 一次成形,或收集到 list 后 join。规则:字符串拼接次数 > 3 时,考虑替代方案。
func f(byval Box b) 里 b 与调用方持有同一个对象——byval 只复制句柄(xvoAddRef),不复制 payload。修改 b.value 对调用方可见。需要独立副本时必须显式 copy(b)。规则:byval 是"按值传递句柄",不是"按值传递对象"。
把 10 万个 int 存入 var 容器,每个 int 都变成堆上 24 字节的 xvalue——总开销 2.4MB + 10 万次 xrtMalloc。同样的数据存入 array<int>(xtarray),元素是连续 int64_t 数组——800KB,一次分配。规则:同质数据永远用静态泛型容器。
class 字段进入 shared 模式后,引用计数是原子的,但容器内容的并发写入不是。两个线程同时 append 同一个 shared list 会数据竞争。shared 解决的是"谁最后释放"的问题,不解决"谁先写入"的问题。规则:shared + 并发写 = 必须 mutex。
本章小结
| 主题 | 核心结论 |
|---|---|
| 性能基线 | 标量运算 = 原生 C 速度;TCC 不做激进优化,峰值性能可交系统编译器 |
| 值存储 | 标量直存(栈/payload),class/容器/var/function 装箱为 xvalue(堆) |
| xvalue | 24 字节固定外壳:Header(Type+RefCount) + Size + 值体 + 扩展 |
| 生命周期 | 原子引用计数 + cgen 确定性清理,无 GC,析构恰好一次 |
| 容器选型 | 同质→xtarray(零装箱),异质→xvalue(逐元素装箱) |
| 字符串 | 不可变 const char*,字面量零分配,临时运算用 xrtTempScope arena |
| 对象 | 一次 xrtMalloc = 外壳 + payload,继承首部布局,析构子类→父类 |
| 闭包 | 环境 record 原子引用计数,调用多一次间接跳转 |
| 派发 | 重载=直接调用(零开销),virtual=vtable 间接跳转,final 可降级 |
| 遥测 | XRT_MEM_DEBUG 启用分配追踪,报告精确到文件:行号 |
复习题
- xvalue_struct 在 64 位平台上占多少字节?Header 里 Type、IsStatic、RefCount 各占几位?为什么把它们编码在同一个 32 位字里?
- 写出
int x = 42和var x = 42在 cgen 产物中的 C 声明。两者的赋值x = x + 1分别生成什么代码? - 解释"确定性清理"的含义。xlang 的清理点由谁决定?与 GC 语言的"最终回收"有什么本质区别?
- array<int> 和 var 容器存储 1000 个整数时,内存占用和分配次数分别是多少?
- xrtTempScope 的分配和释放策略是什么?为什么它比逐条 xrtMalloc/xrtFree 更快?
- class 对象的 payload 在继承链中如何布局?为什么向上转换(Derived→Base)是零开销的?
- 重载函数和 virtual 方法在 cgen 产物中的调用形式有什么不同?各自的运行期开销是什么?
- shared 引用计数保证了什么?不保证什么?多线程并发写 shared 容器需要什么额外措施?
- 内存遥测报告里 live_alloc_count 不为 0 意味着什么?如何定位泄漏的分配点?
- 为什么 byref/out 参数不能被闭包捕获?从内存模型角度解释。
编程练习
写一个函数,包含以下变量:int、float、bool、string、point、array<int>、array<string>、dict<int>、var(装 int)、var(装 string)、一个 class 实例、一个闭包。用 xl cgen --emit 输出生成 C,逐一找出每个变量的 C 声明行,制作"xlang 类型 → C 存储"对照表。验证:哪些是栈上标量?哪些是堆上 xvalue?哪些是 xtarray/xtdict?
参考思路:grep 变量名即可定位声明行;xtarray 和 xvalue 的区别一目了然。
写一个带 destroy 的 class Tracker,在 create 里打印 "created",在 destroy 里打印 "destroyed"。然后写以下场景并预测输出:① 创建后直接离开作用域;② 赋值给第二个变量后离开作用域;③ 存入 array<Tracker> 后离开作用域;④ 重新赋值(b = Tracker(2));⑤ 传入函数(默认参数)后函数返回。运行验证你的预测——如果某处预测错误,用 cgen 取证找出实际的 xvoAddRef/xvoUnref 位置。
参考思路:每次 xvoAddRef 多一个持有者,每次 xvoUnref 少一个;destroyed 只在最后一个 unref 时打印。
写两个函数:typedSum 接收 array<int> 并求和;dynamicSum 接收 var(动态数组)并求和。各传入 100 万个整数,用 time 模块计时对比。然后 xl cgen 取证,找出两个函数遍历循环的生成代码差异——xrtTypedArrayGet vs xvoArrayItemGet。记录时间比值,解释差异来源。
参考思路:typed 版循环体是 int64_t 直取 + 加法;dynamic 版每次取值要解引用 xvalue 外壳再读 vInt。