11.6 字符串缓冲 |
假定你要拼接很多个小的字符串为一个大的字符串,比如,从一个文件中逐行读入字符串。你可能写出下面这样的代码:
-- WARNING: bad code ahead!!
local buff = ""
for line in io.lines() do
buff = buff .. line .. "\n"
end
尽管这段代码看上去很正常,但在Lua中他的效率极低,在处理大文件的时候,你会明显看到很慢,例如,需要花大概1分钟读取350KB的文件。(这就是为什么Lua专门提供了io.read(*all)选项,她读取同样的文件只需要0.02s)
为什么这样呢?Lua使用真正的垃圾收集算法,但他发现程序使用太多的内存他就会遍历他所有的数据结构去释放垃圾数据,一般情况下,这个算法有很好的性能(Lua的快并非偶然的),但是上面那段代码loop使得算法的效率极其低下。
为了理解现象的本质,假定我们身在loop中间,buff已经是一个50KB的字符串,每一行的大小为20bytes,当Lua执行buff..line.."\n"时,她创建了一个新的字符串大小为50,020 bytes,并且从buff中将50KB的字符串拷贝到新串中。也就是说,对于每一行,都要移动50KB的内存,并且越来越多。读取100行的时候(仅仅2KB),Lua已经移动了5MB的内存,使情况变遭的是下面的赋值语句:
buff = buff .. line .. "\n"
老的字符串变成了垃圾数据,两轮循环之后,将有两个老串包含超过100KB的垃圾数据。这个时候Lua会做出正确的决定,进行他的垃圾收集并释放100KB的内存。问题在于每两次循环Lua就要进行一次垃圾收集,读取整个文件需要进行200次垃圾收集。并且它的内存使用是整个文件大小的三倍。
这个问题并不是Lua特有的:其它的采用垃圾收集算法的并且字符串不可变的语言也都存在这个问题。Java是最著名的例子,Java专门提供StringBuffer来改善这种情况。
在继续进行之前,我们应该做个注释的是,在一般情况下,这个问题并不存在。对于小字符串,上面的那个循环没有任何问题。为了读取整个文件我们可以使用io.read(*all),可以很快的将这个文件读入内存。但是在某些时候,没有解决问题的简单的办法,所以下面我们将介绍更加高效的算法来解决这个问题。
我们最初的算法通过将循环每一行的字符串连接到老串上来解决问题,新的算法避免如此:它连接两个小串成为一个稍微大的串,然后连接稍微大的串成更大的串。。。算法的核心是:用一个栈,在栈的底部用来保存已经生成的大的字符串,而小的串从栈定入栈。栈的状态变化和经典的汉诺塔问题类似:位于栈下面的串肯定比上面的长,只要一个较长的串入栈后比它下面的串长,就将两个串合并成一个新的更大的串,新生成的串继续与相邻的串比较如果长于底部的将继续进行合并,循环进行到没有串可以合并或者到达栈底。
function newStack ()
return {""} -- starts with an empty string
end
function addString (stack, s)
table.insert(stack, s) -- push 's' into the the stack
for i=table.getn(stack)-1, 1, -1 do
if string.len(stack[i]) > string.len(stack[i+1]) then
break
end
stack[i] = stack[i] .. table.remove(stack)
end
end
要想获取最终的字符串,我们只需要从上向下一次合并所有的字符串即可。table.concat函数可以将一个列表的所有串合并。
使用这个新的数据结构,我们重写我们的代码:
local s = newStack()
for line in io.lines() do
addString(s, line .. "\n")
end
s = toString(s)
最终的程序读取350KB的文件只需要0.5s,当然调用io.read("*all")仍然是最快的只需要0.02s。
实际上,我们调用io.read("*all")的时候,io.read就是使用我们上面的数据结构,只不过是用C实现的,在Lua标准库中,有些其他函数也是用C实现的,比如table.concat,使用table.concat我们可以很容易的将一个table的中的字符串连接起来,因为它使用C实现的,所以即使字符串很大它处理起来速度还是很快的。
Concat接受第二个可选的参数,代表插入的字符串之间的分隔符。通过使用这个参数,我们不需要在每一行之后插入一个新行:
local t = {}
for line in io.lines() do
table.insert(t, line)
end
s = table.concat(t, "\n") .. "\n"
io.lines迭代子返回不带换行符的一行,concat在字符串之间插入分隔符,但是最后一字符串之后不会插入分隔符,因此我们需要在最后加上一个分隔符。最后一个连接操作复制了整个字符串,这个时候整个字符串可能是很大的。我们可以使用一点小技巧,插入一个空串:
table.insert(t, "")
s = table.concat(t, "\n")