Lua性能优化技巧(三):关于表

一般情况下,你不需要知道Lua实现表的细节,就可以使用它。实际上,Lua花了很多功夫来隐藏内部的实现细节。但是,实现细节揭示了表操作的性能开销情况。因此,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是很有好处的。

Lua的表的实现使用了一些很聪明的算法。每个Lua表的内部包含两个部分:数组部分和哈希部分。数组部分以从1到一个特定的n之间的整数作为键来保存元素(我们稍后即将讨论这个n是如何计算出来的)。所有其他元素(包括在上述范围之外的整数键)都被存放在哈希部分里。

正如其名,哈希部分使用哈希算法来保存和查找键。它使用被称为开放地址表的实现方式,意思是说所有的元素都保存在哈希数组中。用一个哈希函数来获取一个键对应的索引;如果存在冲突的话(意即,如果两个键产生了同一个哈希值),这些键将会被放入一个链表,其中每个元素对应一个数组项。当Lua需要向表中添加一个新的键,但哈希数组已满时,Lua将会重新哈希。重新哈希的第一步是决定新的数组部分和哈希部分的大小。因此,Lua遍历所有的元素,计数并对其进行归类,然后为数组部分选择一个大小,这个大小相当于能使数组部分超过一半的空间都被填满的2的最大的幂;然后为哈希部分选择一个大小,相当于正好能容纳哈希部分所有元素的2的最小的幂。

当Lua创建空表时,两个部分的大小都是0。因此,没有为其分配数组。让我们看一看当执行下面的代码时会发生什么:

代码如下:

local a = {}
for i = 1, 3 do
    a[i] = true
end

这段代码始于创建一个空表。在循环的第一次迭代中,赋值语句

代码如下:

a[1] = true

触发了一次重新哈希;Lua将数组部分的大小设为1,哈希部分依然为空;第二次迭代时

代码如下:

a[2] = true

触发了另一次重新哈希,将数组部分扩大为2.最终,第三次迭代又触发了一次重新哈希,将数组部分的大小扩大为4。

类似下面的代码

代码如下:

a = {}
a.x = 1; a.y = 2; a.z = 3

做的事情类似,只不过增加的是哈希部分的大小。

对于大的表来说,初期的几次重新哈希的开销被分摊到整个表的创建过程中,一个包含三个元素的表需要三次重新哈希,而一个有一百万个元素的表也只需要二十次。但是当创建几千个小表的时候,重新哈希带来的性能影响就会非常显著。

旧版的Lua在创建空表时会预选分配大小(4,如果我没有记错的话),以防止在初始化小表时产生的这些开销。但是这样的实现方式会浪费内存。例如,如果你要创建数百万个点(表现为包含两个元素的表),每个都使用了两倍于实际所需的内存,就会付出高昂的代价。这也是为什么Lua不再为新表预分配数组。

如果你使用C编程,可以通过Lua的API函数lua_createtable来避免重新哈希;除lua_State之外,它还接受两个参数:数组部分的初始大小和哈希部分的初始大小[1]。只要指定适当的值,就可以避免初始化时的重新哈希。需要警惕的是,Lua只会在重新哈希时收缩表的大小,因此如果在初始化时指定了过大的值,Lua可能永远不会纠正你浪费的内存空间。

当使用Lua编程时,你可能可以使用构造式来避免初始化时的重新哈希。当你写下

代码如下:

{true, true, true}

时,Lua知道这个表的数组部分将会有三个元素,因此会创建相应大小的数组。类似的,如果你写下

代码如下:

{x = 1, y = 2, z = 3}

Lua也会为哈希部分创建一个大小为4的数组。例如,执行下面的代码需要2.0秒:

代码如下:

for i = 1, 1000000 do
    local a = {}
    a[1] = 1; a[2] = 2; a[3] = 3
end

如果在创建表时给定正确的大小,执行时间可以缩减到0.7秒:

代码如下:

for i = 1, 1000000 do
    local a = {true, true, true}
    a[1] = 1; a[2] = 2; a[3] = 3
end

但是,如果你写类似于

代码如下:

{[1] = true, [2] = true, [3] = true}

的代码,Lua还不够聪明,无法识别表达式(在本例中是数值字面量)指定的数组索引,因此它会为哈希部分创建一个大小为4的数组,浪费内存和CPU时间。

两个部分的大小只会在Lua重新哈希时重新计算,重新哈希则只会发生在表完全填满后,Lua需要插入新的元素之时。因此,如果你遍历一个表并清除其所有项(也就是全部设为nil),表的大小不会缩小。但是此时,如果你需要插入新的元素,表的大小将会被调整。多数情况下这都不会成为问题,但是,不要指望能通过清除表项来回收内存:最好是直接把表自身清除掉。

一个可以强制重新哈希的猥琐方法是向表中插入足够多的nil。例如:

代码如下:

a = {}
lim = 10000000
for i = 1, lim do a[i] = i end              -- 创建一个巨表
print(collectgarbage("count"))              --> 196626
for i = 1, lim do a[i] = nil end            -- 清除所有元素
print(collectgarbage("count"))              --> 196626
for i = lim + 1, 2 * lim do a[i] = nil end -- 创建一堆nil元素
print(collectgarbage("count"))              --> 17

除非是在特殊情况下,我不推荐使用这个伎俩:它很慢,并且没有简单的方法能知道要插入多少nil才够。

你可能会好奇Lua为什么不会在清除表项时收缩表。首先是为了避免测试写入表中的内容。如果在赋值时检查值是否为nil,将会拖慢所有的赋值操作。第二,也是最重要的,允许在遍历表时将表项赋值为nil。例如下面的循环:

代码如下:

for k, v in pairs(t) do
    if some_property(v) then
        t[k] = nil – 清除元素
    end
end

如果Lua在每次nil赋值后重新哈希这张表,循环就会被破坏。

如果你想要清除一个表中的所有元素,只需要简单地遍历它:

代码如下:

for k in pairs(t) do
    t[k] = nil
end

一个“聪明”的替代解决方案:

代码如下:

while true do
    local k = next(t)
    if not k then break end
    t[k] = nil
end

但是,对于大表来说,这个循环将会非常慢。调用函数next时,如果没有给定前一个键,将会返回表的第一个元素(以某种随机的顺序)。在此例中,next将会遍历这个表,从开始寻找一个非nil元素。由于循环总是将找到的第一个元素置为nil,因此next函数将会花费越来越长的时间来寻找第一个非nil元素。这样的结果是,这个“聪明”的循环需要20秒来清除一个有100,000个元素的表,而使用pairs实现的循环则只需要0.04秒。

[1] 尽管重新哈希算法始终设置数组的大小为2的幂,数组的大小依然可以为任何自然数值。而哈希的大小必须为2的幂,所以第二个参数总是会被圆整为不小于原值的最小的2的幂。

(0)

相关推荐

  • Lua性能优化技巧(六):最后的提示

    正如我们在前言里所说,优化是一个技巧性很强的工作,从程序是否需要优化开始,有若干个方面的内容需要考量.如果程序真的有性能问题,那么我们应该将精力集中于优化哪里和如何优化. 我们在这里讨论的技巧既不是唯一的,也不是最重要的方面.我们在这里专注于讨论专门针对Lua的优化方式,因为有很多其他的方式可以了解通用的程序优化技巧. 在本文结束之前,我还想介绍两种从更大的尺度上优化Lua程序性能的方式,但是它们都牵涉到Lua代码之外的修改.第一个是使用LuaJIT[1],一个Lua的即时编译器,由Mike P

  • Lua性能优化技巧(一):前言

    和在所有其他编程语言中一样,在Lua中,我们依然应当遵循下述两条有关程序优化的箴言: 原则1:不要做优化. 原则2:暂时不要做优化(对专家而言). 这两条原则对于Lua编程来说尤其有意义,Lua正是因其性能而在脚本语言中鹤立鸡群. 当然,我们都知道性能是编程中要考量的一个重要因素,指数级时间复杂度的算法会被认为是棘手的问题,绝非偶然.如果计算结果来得太迟,它就是无用的结果.因此,每一个优秀的程序员都应该时刻平衡在优化代码时所花费的资源和执行代码时所节省的资源. 优秀的程序员对于代码优化要提出的第

  • Lua性能优化技巧(五):削减、重用和回收

    当处理Lua资源时,我们也应该遵循提倡用于地球资源的3R原则--Reduce, Reuse and Recycle,即削减.重用和回收. 削减是最简单的方式.有很多方法可以避免使用新的对象,例如,如果你的程序使用了太多的表,可以考虑改变数据的表述形式.一个最简单的例子,假设你的程序需要操作折线,最自然的表述形式是: 复制代码 代码如下: polyline = {     { x = 10.3, y = 98.5 },     { x = 10.3, y = 18.3 },     { x = 1

  • Lua性能优化技巧(二):基本事实

    在运行任何代码之前,Lua都会把源代码翻译(预编译)成一种内部的格式.这种格式是一个虚拟机指令序列,与真实的CPU所执行的机器码类似.之后,这个内部格式将会被由一个包含巨大的switch结构的while循环组成的C代码解释执行,switch中的每个case对应一条指令. 可能你已经在别处了解到,从5.0版开始,Lua使用一种基于寄存器的虚拟机.这里所说的虚拟机"寄存器"与真正的CPU寄存器并不相同,因为后者难于移植,而且数量非常有限.Lua使用一个栈(通过一个数组和若干索引来实现)来提

  • Lua性能优化技巧(四):关于字符串

    与表类似,了解Lua如何实现字符串可以让你更高效地使用它. Lua实现字符串的方式与多数其他脚本语言所采用的两种主要方式都不相同.首先,Lua中的所有字符串都是内部化[1]的,这意味着Lua维护着任何字符串的一个单一拷贝.当一个新字符串出现时,Lua检查是否有现成的拷贝,如果有的话,重用之.内部化使得诸如字符串对比和索引表之类的操作非常快速,但是会降低创建字符串的速度. 第二,Lua中的变量从不存储字符串,只是引用它们.这种实现方式可以加快很多字符串操作,例如在Perl中,当你写类似于$x=$y

  • Lua性能优化技巧(三):关于表

    一般情况下,你不需要知道Lua实现表的细节,就可以使用它.实际上,Lua花了很多功夫来隐藏内部的实现细节.但是,实现细节揭示了表操作的性能开销情况.因此,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是很有好处的. Lua的表的实现使用了一些很聪明的算法.每个Lua表的内部包含两个部分:数组部分和哈希部分.数组部分以从1到一个特定的n之间的整数作为键来保存元素(我们稍后即将讨论这个n是如何计算出来的).所有其他元素(包括在上述范围之外的整数键)都被存放在哈希部分里. 正如其名,哈

  • Python 代码性能优化技巧分享

    如何进行 Python 性能优化,是本文探讨的主要问题.本文会涉及常见的代码优化方法,性能优化工具的使用以及如何诊断代码的性能瓶颈等内容,希望可以给 Python 开发人员一定的参考. Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构.优化.扩展以及文档相关的事情通常需要消耗 80% 的工作量.优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率. 改进算法,选择合适的数据结构 一个

  • MySQL性能优化技巧分享

    MySQL性能优化 在互联网公司MySQL的使用非常广泛,大家经常会有MySQL性能优化方面的需求.整理了一些在MySQL优化方面的实用技巧. Schema与数据类型优化 整数通常是标识列最好的选择,因为它们很快并且可以使用AUTO_INCREMENT 完全"随机"的字符串(如:MD5().SHA1()或者UUID()等产生的字符串)会任意分布在很大的空间内,会导致INSERT以及一些SELECT语句变的很慢 如果希望查询执行得快速且并发性好,单个查询最好不要做太多的关联查询(互联网公

  • java接口性能优化技巧

    目录 背景 哪些问题会引起接口性能问题 问题解决 慢查询(基于 mysql) ①深度分页 ②未加索引 ③索引失效 ④join 过多 or 子查询过多 ⑤in 的元素过多 ⑥单纯的数据量过大 业务逻辑复杂 ①循环调用 ②顺序调用 线程池设计不合理 锁设计不合理 机器问题(fullGC,机器重启,线程打满) 万金油解决方式 ①缓存 ②回调 or 反查 背景 我负责的系统在去年初就完成了功能上的建设,然后开始进入到推广阶段.随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽. 刚刚收到吐

  • Oracle SQL性能优化系列学习三

    正在看的ORACLE教程是:Oracle SQL性能优化系列学习三.8. 使用DECODE函数来减少处理时间 使用DECODE函数可以避免重复扫描相同记录或重复连接相同的表. 例如: SELECT COUNT(*),SUM(SAL) FROM EMP  WHERE DEPT_NO = 0020  AND ENAME LIKE 'SMITH%'; SELECT COUNT(*),SUM(SAL)  FROM EMP  WHERE DEPT_NO = 0030  AND ENAME LIKE 'SM

随机推荐