在探讨 OpenResty 中 Lua 字符串操作的性能优化技巧前,先说明在 OpenResty 编程中,有关性能方面的重要理念:
OpenResty 作为高并发 Web 服务器,请求生命周期要短,避免长时间占用资源;API 设计要平,单一接口专注单一功能,拆分复杂逻辑;执行过程要快,杜绝主线程阻塞与过量 CPU 运算。
另一关键理念是 “避免产生中间数据”。临时数据的生成与垃圾回收会带来隐性性能损耗。比如多次字符串拼接产生的非最终状态数据,都属于需尽量规避的中间无用数据。本文将用字符串的示例来说明这一点。
字符串不可变
Lua 中字符串具有不可变特性 —— 对字符串的修改(如拼接)并非改变原字符串,而是生成新字符串对象并变更引用,无引用的原字符串会被 GC 回收。
这种设计的优势在于节省内存:相同内容的字符串在内存中仅存一份,多变量可指向同一地址。但频繁新增字符串时,LuaJIT 需通过 lj_str_new 检查字符串是否已存在,不存在则创建新对象,这一过程在高频操作场景下会显著影响性能。
local s = ""
-- for 循环,使用 .. 进行字符串拼接
for i = 1, 100000 do
s = s .. "a"
end
ngx.update_time()
print(ngx.now() - begin)
这段示例是对s 变量做十万次字符串拼接.
使用 table 做一层封装,去掉所有临时的中间字符串,只保留原始数据和最终结果能优化性能。
local begin = ngx.now()
local t = {}
-- for 循环,使用数组来保存字符串,每次都计算数组长度
for i = 1, 100000 do
t[#t + 1] = "a"
end
-- 使用数组的 concat 方法拼接字符串
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
通过 table 依次存储待拼接字符串(以下标#t + 1赋值),最终调用table.concat直接得到结果,可彻底规避临时字符串的生成,省去十万次lj_str_new调用及 GC 损耗。
将 t[#t + 1] = "a" 改为 t[i] = "a",能省去十万次数组长度获取操作。因获取数组长度的时间复杂度为 O (n),成本较高,故通过手动维护下标可直接规避该开销
local begin = ngx.now()
local t = {}
-- for 循环,使用数组来保存字符串,自己维护数组的长度
for i = 1, 100000 do
t[i] = "a"
end
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
除了字符串拼接,OpenResty 中还存在易被忽视的隐蔽临时字符串问题,string.sub 就是典型例子。
因 Lua 字符串不可变,string.sub 截取字符串时会生成新字符串,触发 lj_str_new 调用及后续 GC 操作,例如 string.sub("abcd", 1, 1) 会产生临时字符串。
更优方案是用 string.byte 结合 string.char 实现:string.char(string.byte("abcd")) 先通过 string.byte 获取字符编码,再用 string.char 转回字符,全程无临时字符串生成。
print(string.sub("abcd", 1, 1))
print(string.char(string.byte("abcd")))
利用 SDK 对 table 类型的支持
把上面示例代码的结果,作为响应体的内容输出给客户端可以用以下代码实现:
local t = {}
local index = 1
for i = 1, 100000 do
t[index] = "a"
index = index + 1
end
local response = table.concat(t, "")
ngx.say(response)
OpenResty 的 Lua API ,已经考虑到了这种利用 table 来做字符串拼接的情况,所以,在 ngx.say、ngx.print 、ngx.log、cosocket:send 等这些可能接受大量字符串的 API 中,它不仅接受 string 作为参数,也同时接受 table 作为参数:
local t = {}
local index = 1
for i = 1, 100000 do
t[index] = "a"
index = index + 1
end
ngx.say(t)
省略掉了 local response = table.concat(t, "") 这个字符串拼接的步骤,直接把 table 传给了 ngx.say。这样,就把字符串拼接的任务,从 Lua 层面转移到了 C 层面,又避免了一次字符串的查找、生成和 GC。对于比较长的字符串而言能提升性能。