LOADING

加载过慢请开启缓存 浏览器默认开启

LuaDepth

2025/4/25 Depth Lua

协程

线程:抢占式多任务机制,是一个相对独立的、可调度的执行单元,是系统独立调度和分配CPU的基本单位。它由操作系统来决定执行哪个任务,在运行过程中需要调度,休眠挂起,上下文切换等系统开销,而且最关键还要使用同步机制保证多线程的运行结果正确

协程:协作式多任务机制,协程之间通过函数调用来完成一个既定的任务。它由程序自己决定执行哪个任务,只涉及到控制权的交换(通过resume-yield),同一时刻只有一个协程在运行,而且无法外部停止。通俗来说,协程就是可以用同步的方式,写出异步的代码

协程(Coroutine)拥有4种状态:

  • 运行(running)如果在协程的函数中调用status,传入协程自身的句柄,那么执行到这里的时候才会返回运行状态
  • 挂起(suspended)调用了yeild或还没开始运行,那么就是挂起状态
  • 正常(normal)如果协程A重启协程B时,协程A处于的状态为正常状态
  • 停止(dead)如果一个协程发生错误结束,或正常终止。那么就处于死亡状态(不可以再重启)

Lua的协程是一种非对称式协程,又或叫半协程,因为它提供了两种传递程序控制权的操作:1. 重启调用协程,通过coroutine.resume实现;2. 挂起协程并将程序控制权返回给协程的调用者,即通过coroutine.yield实现。对称式协程,只有一种传递程序控制权的操作,即将控制权直接传递给指定的协程

协程(Coroutine)具有两个非常重要的特性:1. 私有数据在协程间断式运行期间一直有效;2. 协程每次yield后让出控制权,下次被resume后从停止点开始继续执行

闭包

把函数原型换成类,把闭包换成对象。一切就都可以解释了,无论什么时候,把一个函数原型赋值给一个变量时,Lua都会将这个函数原型实例化为一个闭包对象,无论这个闭包对象是否真的用到了所谓的上值表。当在Lua中调用函数或者传递函数对象时,都是在传递闭包对象,而非传递函数原型。
所以说白了,闭包就是函数原型的一个实例
所以它为什么叫闭包呢?就是因为当实例化一个函数原型为一个闭包时,这个函数原型已经捕获到了自己需要所有的上值对象,此后不会再进行任何其他捕获行为。它形成了一个自洽的体系,有自己内部的常量表,上值表,变量表。
这就是Lua的闭包机制,一种用到了OOP思想的机制。

  1. Upvalues是在函数闭包生成的时候(运行到function时)绑定的
  2. Upvalues在闭包还没关闭前(即函数返回前),是对栈的引用,这样做的目的是可以在函数里修改对应的值从而修改Upvalues的值
  3. 闭包关闭后(即函数退出后),Upvalues不再是指针,而是值

userdata

userdata本身只是一个指针(light userdata),或一块受Lua管理的内存块(full userdata),它没有任何预定义行为,在Lua看来它就是一个值,你需要提供配套的C函数去操作它。一般可以用一个对应的元表来判断userdata的类型,以及用一些方法操作userdata的属性

Json

1. 高效的内存管理

  • 预分配内存策略: 使用 membuffer 结构管理缓冲区,提前分配足够空间 (membuffer_ensure_space),避免频繁重分配
  • 内存复用: 尽可能复用内存缓冲区,减少内存分配次数
  • 零拷贝优化: 直接在缓冲区构建结果,避免中间字符串拷贝

2. 优化的解析技术

  • 高效分派: 使用 switch-case 和查表法代替条件分支
  • 内联优化: 关键函数使用 inline,减少函数调用开销
  • 分支预测: 使用 likelyunlikely 宏优化 CPU 分支预测
  • 快速路径: 常见情况直接处理,避免通用路径的开销

3. 数值处理专项优化

  • 整数/浮点分离处理: 对整数和浮点数采用不同的处理路径
  • 延迟转换: 尽量保持整数形式,只在必要时转为浮点数
  • 定界检测: 通过 MAXBY10MAXLASTD 常量快速判断整数边界
  • 幂运算优化: 使用预计算的 powersOf10 表加速指数部分计算

4. 字符串处理优化

  • 转义表: 使用预构建的查表法处理字符转义 (char2escapeescape2char)
  • 直接缓冲区操作: 使用 _unsafe 系列函数在保证安全的情况下跳过边界检查
  • UTF8/16 高效编码: 针对 Unicode 编码进行了特殊优化

5. 智能表处理

  • 数组/对象智能判断: 根据数据特征自动识别是数组还是对象
  • 空表优化: 对空表进行特殊处理,可配置为数组或对象
  • 表遍历优化: 减少表遍历中的 Lua API 调用次数

6. 底层 Lua API 优化

  • 栈操作优化: 最小化 Lua 栈操作,减少入栈/出栈开销
  • 直接数据访问: 直接与 Lua 数据类型交互,避免不必要的类型转换
  • 预分配栈空间: 使用 luaL_checkstack 提前确保足够的栈空间

7. 其他技术优化

  • 基于事件回调: 使用事件回调机制分离解析和构建逻辑
  • 深度控制: 防止过深嵌套导致的栈溢出
  • 条件编译: 根据需要开启格式化或注释支持

这些优化使得 LuaExtend JSON 实现在各种场景下都能保持高性能,尤其在处理大型 JSON 数据或高频解析/序列化场景中优势更加明显。

List

如下面这样的代码:

local t = {}
local tinsert = table.insert
for i = 1, 1000 do
    tinsert(t, i)
end

上面的代码还可以再优化成这样:

local t = {}
for i = 1, 1000 do
    t[#t+1] = i
end

这减少了一次C API的调用,性能会快一点点。

看起来#t并不是立即返回数组长度,而是做了更耗时的操作,翻开Lua的源码,相关的函数是:

lua_Unsigned luaH_getn (Table *t) {
  unsigned int j = t->sizearray;
  if (j > 0 && ttisnil(&t->array[j - 1])) {
    /* there is a boundary in the array part: (binary) search for it */
    unsigned int i = 0;
    while (j - i > 1) {
      unsigned int m = (i+j)/2;
      if (ttisnil(&t->array[m - 1])) j = m;
      else i = m;
    }
    return i;
  }
  /* else must find a boundary in hash part */
  else if (isdummy(t)) {  /* hash part is empty? */
    return j;  /* that is easy... */
  }
  else {
       return unbound_search(t, j);
  }
}

问题就在if (j > 0 && ttisnil(&t->array[j - 1]))这一段,如果数组部分最后一个元素是nil,那么就用二分查找找到最后不是nil的元素,返回这个索引。

我们不断地往Table增加数据时,数组部分会动态地按2的幂扩充,比如现在数组长度是2,再加一个值,数组就动态扩充成4,后面再扩充成8,16…,多出来的槽位填充为nil。所以除非数组刚好满了,否则#t都会用二分查找来确定长度。时间复杂度一样子从O(1)变成了O(logN)。这样分析下来,甚至比Python慢也是可以理解了。

这个问题的根源,我认为是Lua哈希表和数组融合在一起,很多对数组的操作,也不得不考虑哈希部分;而且在rehash的时候,数据经常在数组和哈希表转移。这种融合在我看来一点好处都没有:增加了Table代码的复杂度,且很难针对数组的操作作出更优化的写法。比如没有办法提供数组的精确长度,只能通过二分查找找到最右边的非空元素。

Lua中,把Table当成数组操作,是会比其他语言承担更多的性能开销的。

  • 用户数据设计:使用 lua_newuserdata 创建 C 结构体,配合元表实现对象化

  • 引用系统:通过自定义引用表管理 Lua 值的存储,避免 GC 问题

  • 元表机制:每个模块定义专有元表和元方法,支持面向对象语法

  • 类型检查与转换:使用 luaL_check* 系列函数确保类型安全

  • 动态扩容:如列表/队列等动态数据结构实现自动扩容算法

  • 内存复用:尽可能复用内存,减少分配/释放次数

  • 安全释放:使用 __gc 元方法确保资源自动释放

  • 定制内存分配器:通过 _malloc/_realloc/_free 封装内存操作

序列化

1. 高效的序列化方案

  • 优化的二进制格式设计:使用 type+cookie 8位字节高效编码类型信息
  • 自适应数值编码:根据数值范围自动选择 int8/16/32/64,节省空间
  • 表结构优化:分离数组部分和哈希部分,减少序列化体积
  • 递归深度控制:通过 MAX_DEPTH 参数防止栈溢出
  • 可讨论点:与 JSON/MessagePack 相比的性能优势

2. 内存管理策略

  • 内存分配最小化:使用固定初始缓冲区(INIT_BUFF_SIZE)减少小对象分配
  • 智能扩容算法:缓冲区动态扩容时采用倍增策略,平衡性能与内存使用
  • 内存复用机制:尽可能重用已分配内存,避免频繁分配/释放
  • 可讨论点:在内存受限环境中的优化思路

3. 跨平台兼容性

  • 字节序处理:通过 nativeendian 智能检测并处理大小端差异
  • 整数跨平台表示:处理不同平台整数位宽差异
  • 可讨论点:如何确保数据在异构系统间安全传输

4. C/Lua 接口设计

  • 面向对象包装:使用元表机制提供自然的 OOP 接口
  • 引用系统:通过引用表机制安全管理 Lua 对象生命周期
  • 统一命名空间:所有模块使用 LuaExtendLib 前缀,避免命名冲突
  • 可讨论点:如何平衡接口易用性与底层性能

5. 性能优化技术

  • 内联关键函数:使用 inline 避免高频调用开销
  • 自定义内存分配:通过 _malloc/_realloc/_free 封装,便于优化和跟踪
  • 底层优化:如 JSON 解析器中的字符级优化和查表法处理
  • 算法优化:如快速排序、二分查找等高效算法实现
  • 可讨论点:性能瓶颈分析方法和优化策略