What's new in Lua 5.4

Ben Hoyt

Lua 5.4 新特性

原文由 Ben Hoyt 发布,订阅该博客

Lua 5.4 版本于 6 月底发布,这是这门自 1993 年诞生以来的轻量级脚本语言的第 15 个主要版本。5.4 的新特性包括垃圾回收器的分代模式,对于存在大量短生命周期分配的程序,它能带来更好的性能。该语言现在支持局部变量的“属性”,允许开发者将变量标记为常量(const)或将资源标记为可关闭(close)。此外,相比 5.3,5.4 还有显著的性能提升以及一系列细微改动。

Lua 是一门为嵌入其他应用而优化的编程语言,Redis 和 Adobe Lightroom 等知名软件都在使用它。它也被用作许多电脑游戏的脚本语言,其中不乏《魔兽世界》和《愤怒的小鸟》这样的大作;在2009 年针对游戏行业的一项调查中,Lua 是使用最广泛的脚本语言。Lua 适合嵌入的原因之一在于其小巧:在如今即便最简单的应用下载体积也以数兆计的时代,整个 Lua 5.4 发行版(源码加文档)仅是一个 349KB 的压缩包。以默认配置构建 Lua 解释器时,开发者只需输入 make,等待约五秒完成编译——最终得到的是一个自包含的 200-300KB 二进制文件。

主要版本每隔几年发布一次,并没有固定的发布周期。上一个主要版本 5.3 发布于五年多前的 2015 年 1 月,当时新增了独立的整数类型(此前 Lua 仅使用浮点数)、位运算符、基础的 UTF-8 库以及众多小功能。

语言变化

Lua 5.4 中一个有趣的新特性是新增了局部变量“属性”。在声明局部(块级作用域)变量时,开发者可以在变量名后加上 <const><close> 来赋予其相应属性。const 属性很好理解:与 C 语言中的 const 类似,它表示指定变量在声明初始化后不可再被重新赋值。const 属性并不会让数据结构本身变为不可变:开发者仍然可以修改存储在 const 变量中的表(table)条目,只是不能再次对该变量名进行赋值。const 属性提供了一定程度的编译时安全性,因为如果不小心对常量重新赋值,编译器会报错:

    do
        local x <const> = 42
        x = x+1
    end
    -- ERROR: attempt to assign to const variable 'x'

也许更有用(尽管语法同样不常见)的是 close 属性。它会让 Lua 在变量离开作用域时调用对象的 __close()元方法”。类似于 C++ 中的 RAII 或 Python 中的 with 语句,这是一种以确定性的方式确保内存被释放、文件被关闭或其他资源被正确回收的手段。例如,内置 io.open() 函数返回的文件对象就可以配合 <close> 使用:

    do
        local f <close> = io.open("/etc/fstab", "r")
        -- read from file 'f'
    end
    -- file is automatically closed here

<close> 属性也可用于用户自定义对象:

    function new_thing()
        local thing = {}
        setmetatable(thing, {
            __close = function()
                          print("thing closed")
                      end
        })
        return thing
    end

    do
        local x <close> = new_thing()
        print("use thing")
    end
    -- "thing closed" is printed here after "use thing"

此前,开发者只能为此目的使用 __gc() 元方法,但它只有在对象在稍后某个时间被垃圾回收时才会被调用,而并非在块结束时确定性地触发。

分代垃圾回收

5.4 版本还带来了一种新的分代垃圾回收(GC)模式,它对那些对象生命周期通常较短的特定类型程序表现更好。分代 GC 基于“大多数对象朝生暮死”这一观察,会频繁扫描“年轻”对象,并在其不再被引用时将其释放,而对“年老”对象(即在一或多次 GC 后仍被引用的对象)则扫描得不那么频繁。有趣的是,Lua 的创造者之一 Roberto Ierusalimschy 在 2017 年指出,Lua 之前就有过分代 GC:

Lua 从 5.1 开始就有了增量式垃圾回收器。分代回收器是在 5.2 中作为一项实验引入的,随后在 5.3 中又被移除。它将在 5.4 中再次回归,这一次可能会长期保留。

Ierusalimschy 在 2019 年的一次演讲中(PDF 幻灯片 YouTube 视频)更详细地介绍了增量式 GC 的工作原理,以及为何 5.2 的分代 GC 表现不佳,还有 Lua 团队用什么方案取而代之。在 5.2 的版本中,对象只需经过一次 GC 周期(回收器的一轮扫描)就能成为“老”对象,但在 5.4 中则需要经过两次 GC 周期,这对真实世界的 Lua 程序而言是更准确的模型。两次周期的方案实现起来更复杂,但对许多程序而言能带来更好的 GC 性能——尽管并非对所有程序都有效。Ierusalimschy 指出,构建大型数据结构的程序将不会从中受益。或许正因如此,Lua 团队并未更改默认值:在 5.4 中默认仍使用增量式回收器;开发者需要在程序中加入“collectgarbage("generational")”才能启用分代 GC。

在 lua-l 邮件列表中,Gé Weijers 描述了分代 GC 及其“次要回收”(频繁进行的、用于回收年轻对象的 GC)与新的 <close> 功能(此前称为“toclose”)之间的关联:

5.4 中的垃圾回收器实现了分代模式。如果一个对象在次要回收中存活下来,那么在它变得不可达之后,可能要过很长很长一段时间,它的 __gc 元方法才会被调用,尤其是当你的程序主要创建短生命周期对象时。这使得 __gc 作为简易 RAII 替代方案的用处大打折扣。

新的“toclose”特性对于及时释放资源和解锁要有用得多。

更快

5.4 中一个未被大肆宣传的特性是解释器速度有了显著提升,尽管发布说明中并未提及。在我使用 Gabriel de Quadros Ligneul 的 Lua Benchmarks 套件在 64 位 macOS 机器上进行的一项测试中,我发现 5.4 版本在该套件包含的 11 项基准测试中,平均比 5.3 快 40%:

Lua 基准测试

Elmar Klausmeier 的性能对比也显示了类似的提升。诚然,这两项都是相当人为的基准测试——在游戏引擎这类场景中使用 Lua 时,对性能敏感的代码如图形或矩阵运算无疑会用 C 来编写。即便如此,对于以数值计算为主的代码(这些基准测试大多属于此类),如此幅度的提升也不容小觑。Dibyendu Majumdar 早在 2018 年就在 lua-l 邮件列表中解释了这些改进的部分原因:5.4 新增并优化了用于数值运算的字节码指令,当解析器能够推断相关类型为数值时便会使用这些指令。例如,GETISETI 是两条新指令,用于索引为常量整数时的表查找。

那些需要更高性能的人可以使用 Mike Pall 的 LuaJIT,这是一个针对 Lua 5.1 的即时编译器,比官方 Lua 解释器要快得多。然而,LuaJIT 自 2006 年发布的 5.1 版本之后,就再未跟进 Lua 的任何新特性。这样做将是一项艰巨的工程,因为其中包含了许多破坏兼容性的变更,包括 5.2 中新的作用域规则和 5.3 中新增的整数类型。正因如此,Pall 一直是直言不讳的批评者,批评 Lua 团队所做的向后不兼容改动。

这似乎确实是一个现实问题,而不仅仅是边缘情况:Lua Benchmarks 套件中的两项基准测试在 5.4 中以“C 栈溢出”错误失败(尽管在 5.3 中运行正常),因此我在运行前不得不将其移除。ackfixpoint-fact 这两项基准测试失败,推测是由于 5.4 对递归尾调用的处理方式不同。5.4 中的大多数不兼容之处都已记录在案,但这份清单的长度仍可能给尝试升级大型 Lua 脚本的人带来不小的痛苦。我猜这正是像 Redis 和《魔兽世界》这类需要长期稳定性的工具会锁定在特定旧版本 Lua 上的原因(这两者都使用 5.1 版本)。社区中似乎存在一定程度的分化,一部分人坚持使用 5.1,因为它有 JIT 编译器,而且自那以后的改动相对较小。

Lua 版本之间的不兼容也可能加剧了 Lua 没有统一标准库的问题,LWN 早在二月份就对此进行过报道。如果库作者在新版 Lua 发布时需要做大量工作来完成升级,他们保持更新的积极性就会降低。这就更有可能出现有人为新版 Lua 创建分支,或干脆编写一个新库的情况。

细微改动

除了较大的改动外,Lua 5.4 还新增了许多小功能,包括使用 xoshiro256** 算法的新随机数生成器,以取代原来底层 C 库的 rand() 函数。现在有了一个简单的警告系统,用于在终结器或 __close() 方法出错时发出警告。此外还新增了让具有“userdata”值的 Lua 值拥有多个用户值的能力(userdata 是通过 Lua C API 创建的指向内存块的指针,因此该功能允许由 C 扩展创建的对象关联多个内存块)。

语义上也有一些细微调整:对 for 循环中回绕等边界情况的处理略有不同,以及对整数的字符串到数值强制转换进行了调整(例如,在 5.4 中 "10"+1 的结果是整数 11,而在 5.3 中则是浮点数 11.0)。

总体而言,Lua 在其所处的领域(嵌入到更大型的系统或应用中)是一门相当不错的语言;5.4 版本的发布表明核心团队仍在对其进行持续改进。Lua 没有明确的路线图,因此在这个早期阶段很难知道 5.5 计划有哪些改动,或可能何时发布(Lua 开发者 Pierre Chapuis 甚至推测下一个版本可能会是一个“影响深远的变化”,版本号或为 6.0)。无论如何,5.4 中的新特性对大多数用户来说可能都比较轻微,但性能上的提升无疑是一个可喜的收获。

本文章由 muse-spark-1.2-contributor 进行翻译

评论