我问了一个关于 Lua 性能的问题,其中一个回答是:
您是否研究过保持 Lua 高性能的一般技巧?即知道创建表,而不是创建一个新表,而是重用一个表,使用'local print=print'等来避免全局访问。
这是一个与Lua Patterns,Tips and Tricks略有不同的问题,因为我想要特别影响性能的答案,并且(如果可能)解释为什么会影响性能。
每个答案一个提示将是理想的。
我问了一个关于 Lua 性能的问题,其中一个回答是:
您是否研究过保持 Lua 高性能的一般技巧?即知道创建表,而不是创建一个新表,而是重用一个表,使用'local print=print'等来避免全局访问。
这是一个与Lua Patterns,Tips and Tricks略有不同的问题,因为我想要特别影响性能的答案,并且(如果可能)解释为什么会影响性能。
每个答案一个提示将是理想的。
针对其他一些答案和评论:
确实,作为程序员,您通常应该避免过早的优化。但是。对于编译器没有进行太多优化或根本没有优化的脚本语言,情况并非如此。
所以,每当你在 Lua 中写一些东西,并且经常被执行,在一个时间关键的环境中运行或者可能运行一段时间时,知道要避免(并避免它们)的事情是一件好事。
这是我随着时间的推移发现的集合。其中一些是我在网上发现的,但是当涉及到互联网时,我会怀疑我自己对所有这些都进行了测试。另外,我在 Lua.org 上阅读了 Lua 性能论文。
一些参考:
这是最常见的提示之一,但再次说明它不会受到伤害。
全局变量按其名称存储在哈希表中。访问它们意味着您必须访问表索引。虽然 Lua 有一个非常好的哈希表实现,但它仍然比访问局部变量慢很多。如果您必须使用全局变量,请将它们的值分配给局部变量,这在第二个变量访问时会更快。
do
x = gFoo + gFoo;
end
do -- this actually performs better.
local lFoo = gFoo;
x = lFoo + lFoo;
end
(并不是说简单的测试可能会产生不同的结果。例如,local x; for i=1, 1000 do x=i; end
这里的 for 循环头实际上比循环体花费更多的时间,因此分析结果可能会被扭曲。)
Lua 在创建时对所有字符串进行哈希处理,这使得比较和在表中使用它们非常快,并减少了内存使用,因为所有字符串在内部只存储一次。但它使字符串创建更加昂贵。
避免过多创建字符串的一个流行选项是使用表。例如,如果您必须组装一个长字符串,创建一个表格,将各个字符串放入其中,然后使用table.concat
它来连接一次
-- do NOT do something like this
local ret = "";
for i=1, C do
ret = ret..foo();
end
如果foo()
只返回字符A
,则此循环将创建一系列字符串,如""
, "A"
, "AA"
,"AAA"
等。每个字符串都将被散列并驻留在内存中,直到应用程序完成 - 看到这里的问题了吗?
-- this is a lot faster
local ret = {};
for i=1, C do
ret[#ret+1] = foo();
end
ret = table.concat(ret);
该方法在循环过程中根本不创建字符串,字符串是在函数中创建的foo
,只有引用被复制到表中。之后, concat 创建第二个字符串"AAAAAA..."
(取决于大小C
)。请注意,您可以使用i
代替,#ret+1
但通常您没有这样有用的循环,并且您将没有可以使用的迭代器变量。
我在 lua-users.org 某处发现的另一个技巧是,如果您必须解析字符串,请使用 gsub
some_string:gsub(".", function(m)
return "A";
end);
乍一看,这看起来很奇怪,好处是 gsub 在 C 中“立即”创建了一个字符串,该字符串仅在 gsub 返回时将其传递回 lua 后才进行哈希处理。这避免了表的创建,但可能有更多的函数开销(不是如果你调用foo()
,而是 iffoo()
实际上是一个表达式)
尽可能使用语言结构而不是函数
ipairs
当迭代一个表时,来自 ipairs 的函数开销并不能证明它的使用是合理的。要迭代表,请改为使用
for k=1, #tbl do local v = tbl[k];
它在没有函数调用开销的情况下完全相同(pairs 实际上返回另一个函数,然后为表中的每个元素调用该函数,而#tbl
只计算一次)。即使您需要价值,它也快得多。如果你不...
Lua 5.2 的注意事项:在 5.2 中,您实际上可以在元表中定义一个__ipairs
字段,这在某些情况下确实有用。ipairs
但是,Lua 5.2 也使该__len
字段适用于表,因此您可能仍然更喜欢上面的代码,ipairs
因为__len
元方法只调用一次,而ipairs
每次迭代都会得到一个额外的函数调用。
table.insert
,table.remove
table.insert
和的简单用法table.remove
可以通过使用#
运算符来代替。基本上这是用于简单的推送和弹出操作。这里有些例子:
table.insert(foo, bar);
-- does the same as
foo[#foo+1] = bar;
local x = table.remove(foo);
-- does the same as
local x = foo[#foo];
foo[#foo] = nil;
对于班次(例如table.remove(foo, 1)
),如果不希望以稀疏表结束,那么使用表函数当然更好。
您可能(也可能不会)在代码中做出如下决定
if a == "C" or a == "D" or a == "E" or a == "F" then
...
end
现在这是一个完全有效的案例,但是(根据我自己的测试)从 4 个比较开始并排除表生成,这实际上更快:
local compares = { C = true, D = true, E = true, F = true };
if compares[a] then
...
end
并且由于哈希表具有恒定的查找时间,因此每次额外比较都会增加性能增益。另一方面,如果“大多数时候”有一个或两个比较匹配,则使用布尔方式或组合可能会更好。
这在Lua Performance Tips中有详细的讨论。基本上问题在于 Lua 会按需分配您的表,并且这样做实际上比清理它的内容并再次填充它要花费更多的时间。
然而,这有点问题,因为 Lua 本身并没有提供从表中删除所有元素的方法,而且pairs()
它本身也不是性能野兽。我自己还没有对这个问题进行任何性能测试。
如果可以,定义一个清除表的 C 函数,这应该是表重用的一个很好的解决方案。
这是最大的问题,我想。虽然非解释性语言的编译器可以轻松优化大量冗余,但 Lua 不会。
在 Lua 中使用表可以很容易地完成。对于单参数函数,您甚至可以用表和 __index 元方法替换它们。尽管这会破坏透明度,但由于少了一个函数调用,缓存值的性能会更好。
这是使用元表对单个参数进行记忆化的实现。(重要提示:此变体不支持nil 值参数,但对于现有值来说非常快。)
function tmemoize(func)
return setmetatable({}, {
__index = function(self, k)
local v = func(k);
self[k] = v
return v;
end
});
end
-- usage (does not support nil values!)
local mf = tmemoize(myfunc);
local v = mf[x];
您实际上可以针对多个输入值修改此模式
这个想法类似于memoization,即“缓存”结果。但是这里不是缓存函数的结果,而是通过将中间值的计算放入构造函数中来缓存中间值,该构造函数在其块中定义计算函数。实际上,我只会称其为巧妙地使用闭包。
-- Normal function
function foo(a, b, x)
return cheaper_expression(expensive_expression(a,b), x);
end
-- foo(a,b,x1);
-- foo(a,b,x2);
-- ...
-- Partial application
function foo(a, b)
local C = expensive_expression(a,b);
return function(x)
return cheaper_expression(C, x);
end
end
-- local f = foo(a,b);
-- f(x1);
-- f(x2);
-- ...
这样就可以轻松创建灵活的函数来缓存他们的一些工作,而不会对程序流程产生太大影响。
一个极端的变体是Currying,但这实际上更像是一种模仿函数式编程的方法。
这是一个更广泛(“真实世界”)的示例,其中有一些代码遗漏,否则它很容易在这里占据整个页面(即get_color_values
实际上做了很多值检查并识别接受混合值)
function LinearColorBlender(col_from, col_to)
local cfr, cfg, cfb, cfa = get_color_values(col_from);
local ctr, ctg, ctb, cta = get_color_values(col_to);
local cdr, cdg, cdb, cda = ctr-cfr, ctg-cfg, ctb-cfb, cta-cfa;
if not cfr or not ctr then
error("One of given arguments is not a color.");
end
return function(pos)
if type(pos) ~= "number" then
error("arg1 (pos) must be in range 0..1");
end
if pos < 0 then pos = 0; end;
if pos > 1 then pos = 1; end;
return cfr + cdr*pos, cfg + cdg*pos, cfb + cdb*pos, cfa + cda*pos;
end
end
-- Call
local blender = LinearColorBlender({1,1,1,1},{0,0,0,1});
object:SetColor(blender(0.1));
object:SetColor(blender(0.3));
object:SetColor(blender(0.7));
您可以看到,一旦创建了搅拌机,该函数只需检查单个值而不是最多八个。我什至提取了差异计算,虽然它可能没有太大改进,但我希望它能显示这种模式试图实现的目标。
如果您的 lua 程序真的太慢,请使用 Lua 分析器并清理昂贵的东西或迁移到 C。但是如果您不坐在那里等待,那么您的时间就被浪费了。
优化第一定律:不要。
我很乐意看到一个问题,您可以在 ipairs 和pairs 之间进行选择,并且可以衡量差异的影响。
一个容易实现的目标是记住在每个模块中使用局部变量。一般不值得做类似的事情
本地 strfind = string.find
除非你能找到一个测量结果告诉你。
还必须指出,使用表中的数组字段比使用任何类型的键的表要快得多。它发生(几乎)所有 Lua 实现(包括 LuaJ)在表中存储一个称为“数组部分”,由表数组字段访问,并且不存储字段键,也不查找它;)。
您甚至可以模仿其他语言的静态方面,如struct
C++/Javaclass
等。本地和数组就足够了。
保持表格简短,表格越大,搜索时间越长。并且在同一行中迭代数字索引表(=数组)比基于键的表快(因此 ipairs 比对快)