优化Lua字符串字面量以节省400字节
摘要
一篇技术博客文章,探讨在ComputerCraft程序环境下,通过选择最优表示方式(引号字符串与原始字符串)来减小Lua字符串字面量大小的方法,实现了400字节的节省。
暂无内容
查看缓存全文
缓存时间: 2026/07/16 19:53
# 优化 Lua 字符串字面量以节省 400 字节
来源:https://purplesyringa.moe/blog/guest/optimizing-lua-string-literals-to-save-400-bytes/
2026年7月2日 客座文章 (https://sylfn.codeberg.page/26w27a-optimizing-lua-string-literals/)
这是一篇由 Yuki (https://codeberg.org/sylfn/) 撰写的客座文章,介绍我们在共享的 ComputerCraft 爱好者项目 (https://github.com/purplesyringa/computercraft-programs/) 中用于 Lua 代码压缩的一些技巧。我之前写过我们如何为此目的适配 bzip2 (https://purplesyringa.moe/blog/an-ode-to-bzip/);这篇故事是同一背景下的一篇续作。
如果你想阅读更多她的文章,这里有 Atom 订阅源 (https://sylfn.codeberg.page/atom.xml) 和博客 (https://sylfn.codeberg.page/)。
### 背景
我和我的女朋友为 CC: Tweaked (https://github.com/purplesyringa/computercraft-programs) 创建了一堆很酷的程序。CC:T 的程序是用 Lua 编写的,而游戏内的计算机磁盘空间有限。未调整的限制是每台计算机(或海龟)1 MB 的磁盘空间,以及每张软盘 125 KB 的磁盘空间。
我们的程序远远超出了这个限制。在撰写本文时,仓库中所有 lua 代码及其文档的连接后大小略大于 440 KB,这无法放入一张软盘,而且实在太大。
不过,我们有一个解决大小问题的方案,那就是压缩。我们的数据主要是 Lua 代码和文本,而有一些压缩算法 (https://purplesyringa.moe/blog/an-ode-to-bzip/) 在这方面是(在输出大小和解压速度方面)最佳。
我们的目标之一是创建一个自解压存档,形式为单个文件,这样安装只需在 shell 中输入:
``
wget https://cc.purplesyringa.moe/initrd.lua startup.lua
``
### 替代?
这一壮举需要将数据与解压器合并。最笨的方法是将数据序列化为 Lua 字面量,但这有一些问题。序列化 `nil`、`true`、`false`、数字和列表是极其简单的。困难的部分是表和字符串。
表有两种主要的键表示方法:基于标识符和基于表达式。这两个表是等价的:
``
{ key = 10 }
{ ["key"] = 10 }
``
第一种使用基于标识符的键,这只能是有效的标识符,然后被转换为字符串;第二种是通配变体。我没有测试过是否值得到处使用基于表达式的键,但我感觉不值得。
替代的最终 Boss 是字符串。
这能有多难呢?
### 字符串是困难的
有一个梗是关于 YAML 中有几十种创建字符串的方法,以及它如何让不熟悉的人感到困惑。这种情况有点像这样。
Lua 字符串(特别是 Cobalt(CC 中使用的 Lua 运行时)中的字符串)是字节字符串,类似于 Python 中的 `b"meow"` 但更糟,或者像 Rust 中的 `b"meow"` 但更好。有三种主要方式编写字符串:
1. 单引号,单行:`'meow'`。反斜杠转义有效。
2. 双引号,单行:`"meow"`。反斜杠转义有效。
3. 原始字符串,可多行:`[[meow]]` 和 `[=[this [[is]] a string]=]`。反斜杠被视为普通字符。
带引号的字符串很酷。如果字符串不包含任何换行或特殊字符,那么带引号的表示法显然是最好的。这可能是你编写大多数正常字符串的方式。然而,原始字符串……
### 原始字符串
原始字符串以 `[=[`(任意数量的 `=`)开始,以 `]=]`(相同数量的 `=`)结束,并且其内容不能包含起始符或结束符。这使得 `[=[meow [=[ mrrp]=]` 是一个无效字符串,但 `[[meow [=[ mrrp]]` 是有效的。
有人可能会想:“好吧,让我们找到 `\[=\+\[|\]=\+\]` 正则表达式的最长匹配,然后比它们多一个 = 号”,但这在 `[=[==[` 上会失败。
有人可能会想:“好吧,让我们通过下一个字符来过滤 `[\[\]]=*` 的匹配,然后比它们多一个 = 号”,这基本上能行……大部分时候。
但这里还有细微差别。
### 细微差别???
表键中的原始字符串是特殊的,需要空格,就像 C++11 之前的 C++ 模板:
``
{ [ [[meow]]] = 10 }
``
这是最容易处理的细微差别之一,但还有更多。
什么是换行符?有人甚至可能说它是一个序列。Cobalt 在原始字符串中将换行序列(CRLF、LFCR、单个 CR、单个 LF)折叠成单个 LF,这就像对你的 PNG 运行 `dos2unix` 并期望它不会损坏一样。
而 Cobalt 还会做一件事。如果字符串以换行符开头,该换行符会被丢弃。只有第一个换行符受到影响,所以这两条都是 true:
``
print("meow" == [[
meow]])
print("\nmeow" == [[
meow]])
``
### 处理 CR
所以我们终究还是需要转义。但原始字符串之所以称为原始,是因为它们不以任何特殊方式解释反斜杠!我们该如何转义?
我们有一个工具,叫做 `gsub` (https://www.lua.org/manual/5.2/manual.html#pdf-string.gsub)。
如果字符串中有一个未使用的字符(对于文本文件,大约是 `\0`),那么我们可以将所有 CR 替换为该字符,然后使用 gsub,就像这样:
``
print("\r\r\r" == ([[RRR]]):gsub('R','\r'))
``
注意,因为我们在普通字符串以及作为 `gsub` 的参数中使用该字符,所以某些字符被禁止用作“转义”字符,即:
- CR 本身
- `\`,正常的转义字符
- 任何 `^$()%.\[\]*+-?`,这些都是特殊的模式字符
- 单引号(我们在 Rust 端使用它作为字符串边界,使代码更易读)
简单的情况已经用完了,我们现在有一个包含各种字节的字节序列,甚至包括 CR,而且所有这些都需要保留。
处理这个问题的一种方法是同时替换 CR 和我们的转义字符(例如 `E`)为其他东西,例如:
- `Er` 表示 CR
- `E]` 表示转义字符本身
这种解决方案有效,但如果字符串具有近乎均匀的字节分布,则字符串的大小会增加大约 1%。
还有另一种方法。我们可以将“它实际上是 CR 吗”的信息带外存储,然后通过 `gsub` 应用:
``
([[string]]):gsub('E', (function(i)
return function()
i = i + 1
return bit32.btest(
('V'):byte(math.floor(i/7)),
2^(i%7)
) and '\r'
end
end)(6))
``
其中 `E` 是转义字符,`V` 是位串。
这大约一百字节(为可读性未压缩)充满了技巧:
- 只访问 `V` 的低 7 位,这样我们总能设置最高位,避免撞上特殊字符。
- 使用 `and '\r'` 将转义替换为 CR,因为如果 `gsub` 回调返回 `nil` 或 `false`,`gsub` 不会执行替换。
- `function(i)` 创建了一个局部变量,在调用点初始化为 `6`。
- `i = 6` 是初始状态,以补偿 Lua 中基于 1 的索引。
这几乎将 CR 开销减半,因为现在 CR/转义用大约 9 位编码,而不是 16 位。
可能有进一步简化此过程的方法,如果你知道,请告诉我。
### 额外:Rust 视角
还有一个技巧:
``
core::iter::repeat(0..7).flatten()
``
创建一个迭代器,生成 `0, 1, ..., 7, 0, 1, ..., 7, 0, 1, ...`,即我们需要填充的位的索引。这比使用 `% 7` 和 `.enumerate()` 感觉更简洁。
完整的字符串序列化代码可以在这里访问 (https://github.com/purplesyringa/computercraft-programs/blob/df3551a486e239f7761312ba8989560229ddd241/initrd-ng/initrd-core/src/ser.rs#L102)。
相似文章
Tiny-Lua-Compiler: 可能是有史以来最小的 Lua 编译器
Tiny-Lua-Compiler 是一个用于教学的、自举的 Lua 5.1 编译器和虚拟机,完全用纯 Lua 编写。其设计目标是体积足够小以便于研究,同时又功能完备到足以处理真实的语言特性。
Lua作为一种实用的“软基石”语言
本文讨论了Lua作为一种实用的“软基石”语言,探讨了它在软件开发中的作用。
Zig ELF 二进制文件代码高尔夫 (2025)
深入技术探讨如何缩小 Zig ELF 二进制文件的大小,从 2180K 缩减至 500 字节以下,通过去除调试信息、切换到 ReleaseSmall 以及使用 freestanding 目标。
@tom_doerr: 将 Claude Code 和 Cursor 的 token 成本降低 60-95% https://github.com/yvgude/lean-ctx
lean-ctx 是一个基于 Rust 的开源上下文运行时,通过文件读取压缩和 Shell 输出优化,将 Claude Code、Cursor、Copilot 等 AI 编程助手的 token 成本降低 60–95%。它以 Shell Hook 和 MCP Server 的形式运行,提供 56 个工具及多种读取模式。
@DataChaz: 停止浪费你的Token!如果你使用Claude Code,你可能浪费了80%的上下文窗口。我找到了10个超棒工具…
由@DataChaz发布的一条推文列出了10个开源工具,用于大幅降低Claude Code及类似AI编程助手的Token使用量,通过各种优化手段可将API费用削减75%-98%。