在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
来源 https://segmentfault.com/a/1190000004372649 此为 Lua Programming Gems 一书的第二章:Lua Performance Tips,作者为 Roberto Ierusalimschy。 我的翻译以 网上别人的翻译 为基础,做了比较大的修改,读起来更通顺。 关于性能优化的两条格言:
不要在缺乏恰当度量(measurements)时试图去优化软件。编程老手和菜鸟之间的区别不是说老手更善于洞察程序的性能瓶颈,而是老手知道他们并不善于此。 做性能优化离不开度量。优化前度量,可知何处需要优化。优化后度量,可知「优化」是否确实改进了代码。 基本事实运行代码之前,Lua 会把源代码翻译(预编译)成一种内部格式,这种格式由一连串虚拟机的指令构成,与真实 CPU 的机器码很相似。接下来,这一内部格式交由 C 代码来解释,基本上就是一个 也许你已从他处得知,自 5.0 版起,Lua 使用了一个基于寄存器的虚拟机。这些「寄存器」跟 CPU 中真实的寄存器并无关联,因为这种关联既无可移植性,也受限于可用的寄存器数量。Lua 使用一个栈(由一个数组加上一些索引实现)来存放它的寄存器。每个活动的(active)函数都有一份活动记录(activation record),活动记录占用栈的一小块,存放着这个函数对应的寄存器。因此,每个函数都有其自己的寄存器。由于每条指令只有 8 个 bit 用来指定寄存器,每个函数便可以使用多至 250 个寄存器。 Lua 的寄存器如此之多,预编译时便能将所有的局部变量存到寄存器中。所以,在 Lua 中访问局部变量是很快的。举个例子, 如果
所以,不难证明,要想改进 Lua 程序的性能,最重要的一条原则就是:使用局部变量(use locals)! 除了一些明显的地方外,另有几处也可使用局部变量,可以助你挤出更多的性能。比如,如果在很长的循环里调用函数,可以先将这个函数赋值给一个局部变量。这个代码:
比如下代码慢 30%:
访问外层局部变量(也就是外一层函数的局部变量)并没有访问局部变量快,但是仍然比访问全局变量快。考虑如下代码:
我们可以通过在
第二段代码比第一段快 30%。 与其他语言的编译器相比,Lua 的编译器算是比较高效的,尽管如此,编译仍是一项繁重的任务。所以,应尽量避免在程序中编译代码(比如,使用 举个例子,下面的代码创建一个包含 10000 个函数的表,表中的函数分别返回常量
这段代码运行了 1.4 秒。 使用闭包,可以避免动态编译。下面的代码创建同样的 10000 个函数只用了 1/10 的时间(0.14秒):
关于表通常,使用表(table)时并不需要知道它的实现细节。事实上,Lua 尽力避免把实现细节暴露给用户。然而这些细节还是在表操作的性能中暴露出来了。所以,为了高效地使用表,了解一些 Lua 实现表的方法,不无益处。 Lua 实现表的算法颇为巧妙。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存的项(entry)以整数为键(key),从 1 到某个特定的 n,(稍后会讨论 n 是怎么计算的。)所有其他的项(包括整数键超出范围的)则保存在哈希部分。 顾名思义,哈希部分使用哈希算法来保存和查找键值。它使用的是开放寻址(open address)的表,意味着所有的项都直接存在哈希数组里。键值的主索引由哈希函数给出;如果发生冲突(两个键值哈希到相同的位置),这些键值就串成一个链表,链表的每个元素占用数组的一项。 当 Lua 想在表中插入一个新的键值而哈希数组已满时,Lua 会做一次重新哈希(rehash)。重新哈希的第一步是决定新的数组部分和哈希部分的大小。所以 Lua 遍历所有的项,并加以计数和分类,然后取一个使数组部分用量过半的最大的 2 的指数值,作为数组部分的大小。而哈希部分的大小则是一个容得下剩余项(即那些不适合放在数组部分的项)的最小的 2 的指数值。 当 Lua 创建一个空表时,两部分的大小都是 0,因此也就没有为它们分配数组空间。看看如下代码运行时会发生些什么:
一开始创建一个空表。循环的第一次迭代时,赋值语句 像下面这样的代码:
做的事情类似,大小增长的却是表的哈希部分。 对于大型的表,这些初始的开销将会被整个创建过程平摊:创建 3 个元素的表需要进行 3 次重新哈希,而创建一百万个元素的表只需要 20 次。但是当你创建几千个小表时,总开销就会很显著。 老版的 Lua 在创建空表时会预分配一些空位(如果没记错,是 4),来避免这种创建小表时的初始开销。不过,这样又有浪费内存之嫌。比如,以仅有两个项的表来表示点,每个点使用的内存就是真正所需内存的两倍,那么创建几百万个点将会使你付出高昂的代价。这就是现在 Lua 不为空表预分配空位的原因。 如果你用的是 C,可以通过 Lua 的 API 函数 如果你用的是 Lua,可以通过构造器(constructors)来避免那些初始的重新哈希。当你写下
如果以正确的大小来创建这个表,运行时间就降到了 0.7 秒:
然而,当你写下形如 表的两个部分的大小只在表重新哈希时计算,而重新哈希只在表已全满而又需要插入新元素时才会发生。因此,当你遍历一个表并把个中元素逐一删除时(即设它们为 有一则强制重新哈希的奇技淫巧,即往表里插入足够的
除非特殊情况需要,我并不推荐这种手法,因为这样做很慢,而且要知道多少元素才算「足够」,也没有简单易行的方法。 你可能会想,Lua 为什么不在我们插入
如果 Lua 在 如果你想删除表中的所有元素,正确的方法是使用一个简单的循环:
或者使用"聪明"一点的方法:
不过,这个循环在表很大时会很慢。调用函数 关于字符串和表一样,了解 Lua 实现字符串的细节对高效地使用字符串也会有所帮助。 Lua 实现字符串的方式和大多数其他的脚本语言有两点重要的区别。其一,Lua 的字符串都是内化的(internalized);这意味着字符串在 Lua 中都只有一份拷贝。每当一个新字符串出现时,Lua 会先检查这个字符串是否已经有一份拷贝,如果有,就重用这份拷贝。内化(internalization)使字符串比较及表索引这样的操作变得非常快,但是字符串的创建会变慢。 其二,Lua 的字符串变量从来不会包含字符串本身,包含的只是字符串的引用。这种实现加快了某些字符串操作。比如,对 Perl 来说,如果你写下这样的语句: 这种使用引用的实现,使某种特定形式的字符串连接变慢了。在 Perl 里,
如果将 Lua 并没有提供这第二种较快的方法,因为 Lua 的变量并没有与之关联的缓冲区。所以,我们必须使用一个显式的缓冲区:包含字符串片段的表就行。以下循环还是读 5MB 的文件,费时 0.28 秒。没 Perl 那么快,不过也不赖。
减少,重用,回收当处理 Lua 资源时,我们应当遵守跟利用地球资源一样的 3R 原则。 减少(reduce)是最简单的一种途径。有几种方法可以避免创建对象。例如,如果你的程序使用了大量的表,或许可以考虑改变它的数据表示。举个简单的例子,假如你的程序需要处理折线(polyline)。在 Lua 里,折线最自然的表示是使用一个点的列表,像这样:
这种表示虽然自然,折线较大时却不经济,因为每个点都要用一个表。下面这种表示改用数组,内存略为节省:
对于一条有一百万个点的折线,这种改变使内存用量从 95KB 降到 65KB。当然,作为代价,程序的可读性有所损失: 还有一个更经济的方法,用两个列表,一个存
之前的 循环是寻找降低不必要资源创建的好地方。例如,如果在循环中创建了一个常量的(constant)表,便可以把表移到循环之外,或者甚至可以移到外围函数之外。比较如下两段代码:
同样的技巧也可以用于闭包,只要移动时不致越出闭包所需变量的作用域。例如,考虑以下函数:
只要将内部(inner)函数移到循环之外,就可避免为每一行都创建一个新的闭包:
不过,不能将函数 很多字符串的处理,都可以通过在现有字符串上使用下标,来避免创建不必要的新字符串。例如,函数 即使不能避免使用新的对象,也可以通过 重用(reuse)来避免创建新的对象。对字符串来说,重用是没有必要的,因为 Lua 已经替我们这样做了:所有的字符串都是内化的(internalized),因此只要可能就会重用。对表来说,重用就显得卓有成效了。举一个常见的例子,让我们回到在循环内创建表的情况。不同的是,这次的表是可变的(not constant)。不过,往往只需简单的改变内容,还是可以在所有的迭代中重用同一个表的。考虑以下代码:
以下代码与之等价,但是重用了表:
实现重用的一种特别有效的方法是记忆化(memoizing)。基本想法非常简单:对于一个给定的输入,保存其计算结果,当遇到同样的输入时,程序只需重用之前保存的结果。 来看看 记忆化方法的一个比较普遍的问题是,保存之前结果而在空间上的花费可能会甚于重用这些结果的好处。为了解决这个问题,我们可以使用弱表(weak table),这样,不用的结果最后就会从表中删除。 借助于高阶函数(higher-order functions),我们可以定义一个通用的记忆化函数:
对于一个给定的函数
新函数的使用方式和老函数一样,但是如果我们加载的字符串中有很多重复的字符串,便会获得很大的性能提升。 如果你的程序创建和释放过多的协程(coroutines),也许可以通过 回收(recycle)来提高它的性能。目前协程的 API 并没有直接提供重用协程的方法,但是我们可以设法克服这一限制。考虑以下协程:
这个协程接受一个作业(job)(一个待执行的函数),执行这个作业,结束后等待下一个作业。 Lua 中的大多数回收都是由垃圾收集器自动完成的。Lua 使用一个增量(incremental)的垃圾收集器,逐步(in small steps)回收(增量地),跟程序一起交错执行。每一步回收多少,跟内存分配成正比:Lua 分配了多少内存,垃圾收集器就做多少相应比例的工作。程序消耗内存越快,收集器尝试回收内存也就越快。 如果我们在程序中遵守减少和重用的原则,收集器通常没有太多的事情可做。但是有时候我们不能避免创建大量的垃圾,这时收集器就可能变得任务繁重了。Lua 的垃圾收集器是为一般的程序而设的,对大多数应用来说,它的表现都是相当不错的。但是有时候,某些特殊的应用场景,适当地调整收集器还是可以提高性能的。 要控制垃圾收集器,可以调用 Lua 的函数 函数 对于某些批处理程序(batch programs),可以考虑「永远」地停止收集器。这些批处理程序通常都是先创建一些数据结构,并根据那些结构体产生一些输出,然后就退出(比如编译器)。对于那些程序,试图去收集垃圾也许就比较浪费时间了,因为没什么垃圾可回收的,并且程序一旦退出,所有的内存就会得到释放。 对于非批处理的程序,永远停止收集器并不可取。不过,在一些关键的时间点,停止收集器对程序可能却是有益的。如有必要,还可以由程序来完全控制垃圾收集器,让它总是处于停止状态,只在程序显式地要求执行一个步骤或者执行一个完整的回收时,收集器才开始工作。例如,有些事件驱动的平台会提供一个 最后一个方法,可以试着改变收集器的参数。收集器由两个参数控制其收集的步长(pace)。第一个是 这些参数对一个程序的总体性能的影响是很难预测的。收集器越快,其每秒耗费的 CPU 周期显然也就越多;但是另一方面,或许这样能减少程序的内存使用总量,从而减少换页(paging)。只有通过仔细的实验,才能为这些参数找到最佳的值。
|
请发表评论