我可以做些啥来提高 Lua 程序的性能?

Posted

技术标签:

【中文标题】我可以做些啥来提高 Lua 程序的性能?【英文标题】:What can I do to increase the performance of a Lua program?我可以做些什么来提高 Lua 程序的性能? 【发布时间】:2010-09-14 08:33:55 【问题描述】:

我问了一个关于 Lua 性能的问题,并在responses 上问:

您是否研究过保持 Lua 高性能的一般技巧?即知道表的创建并重用表而不是创建新表,使用“本地打印=打印”等以避免全局访问。

这是与Lua Patterns,Tips and Tricks 略有不同的问题,因为我希望得到具体影响性能的答案,并且(如果可能)解释为什么会影响性能。

每个答案一个提示将是理想的。

【问题讨论】:

lua-users.org/wiki/OptimisationTips">Lua优化技巧 trac.caspring.org/wiki/LuaPerformance 【参考方案1】:

针对其他一些答案和cmets:

确实,作为程序员,您通常应该避免过早优化。 但是。对于编译器没有进行太多优化或根本没有优化的脚本语言来说,情况并非如此。

因此,每当您在 Lua 中编写某些内容时,并且经常执行、在时间要求严格的环境中运行或可能运行一段时间时,了解要避免的事情是一件好事(并避免它们)。

这是我在一段时间内发现的内容的集合。其中一些是我在网上找到的,但是当 interwebs 担心时,我会怀疑我自己对所有这些都进行了测试。另外,我在 Lua.org 上阅读了 Lua 性能论文。

一些参考:

Lua Performance Tips Lua-users.org Optimisation Tips

避免全局变量

这是最常见的提示之一,但再次说明它不会有坏处。

全局变量按其名称存储在哈希表中。访问它们意味着您必须访问表索引。虽然 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(),但如果foo()实际上是一个表达式)

避免函数开销

尽可能使用语言结构而不是函数

函数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 元方法只调用一次,而 @987654353 @你每次迭代都会得到一个额外的函数调用。

函数table.insert, table.remove

table.inserttable.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)),如果不希望以稀疏表结尾,那么使用表函数当然更好。

使用表进行 SQL-IN 相似比较

您可能(也可能不会)在您的代码中做出如下决定

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];

您实际上可以针对多个输入值修改此模式

Partial application

这个想法类似于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));

您可以看到,一旦创建了搅拌机,该函数只需检查单个值,而不是最多八个。我什至提取了差异计算,虽然它可能没有太大改进,但我希望它能显示这种模式试图实现的目标。

【讨论】:

做得很好。我不是 100% 确定我理解“函数构造函数”部分。这可能是因为数学似乎没有任何实际意义。你能提供一个实际的例子吗?但除此之外,这是一份出色的清单,值得一试。 @JonEricson 感谢您的反馈,我将最后一部分重命名为“参数化函数”,我认为这是一个更好的名称,因为我也不满意。 "Use tables for SQL-IN alike compares" -- 在我看来,应该建议相反,尽可能多地使用条件来避免创建表,除非查找表是持久的或者它只是一次性的事情。 @Alundaio 显然,具有许多 OR 的静态 if 构造应该被持久表替换,在每个函数调用上创建表都会破坏目的。我想我的例子可能会被误解为每次都应该构建它。我会努力找时间解决的,谢谢! 还有local function ...【参考方案2】:

如果您的 lua 程序真的太慢,请使用 Lua 分析器并清理昂贵的东西或迁移到 C。但是如果您不坐在那里等待,那么您的时间就被浪费了。

优化第一定律:不要。

我很想看到一个问题,您可以在 ipairs 和pairs 之间进行选择,并且可以衡量差异的影响。

一个容易实现的目标是记住在每个模块中使用局部变量。一般不值得做这样的事情

本地 strfind = string.find

除非你能找到一个可以告诉你的测量结果。

【讨论】:

你在为 MS 工作吗?考虑到记事本明显比大约 30 年前的整个军事和太空控制综合体更强大的硬件,他们似乎把你的“法律”放在心上。 是的,我知道,它被滥用来创建缓慢的软件,然后返回。也许它对 Don 自己有用,他总是可以断言什么需要优化,什么不需要。对于 90% 的程序员来说,它读作“无论你的愚蠢算法有多慢,都不要优化它”。这就是为什么我不喜欢它被传播的原因。【参考方案3】: 使最常用的函数本地化 充分利用表作为 HashSets 通过重用减少表创建 使用 luajit!

【讨论】:

【参考方案4】:

还必须指出,使用表中的数组字段比使用任何类型的键的表要快得多。它发生(几乎)所有 Lua 实现(包括 LuaJ)在表中存储一个称为“数组部分”,由表数组字段访问,并且不存储字段键,也不查找它;)。

您甚至还可以模仿其他语言的静态方面,例如 struct、C++/Java class 等。本地和数组就足够了。

【讨论】:

【参考方案5】:

保持表格简短,表格越大,搜索时间越长。 在同一行中,迭代数字索引表(=arrays)比基于键的表快(因此 ipairs 比对快)

【讨论】:

Lua lua 表是数组和 hastables 的组合,通过索引访问任一部分具有恒定的时间复杂度 (O(1))。 O(1) 隐藏了一个常数 k。他们在这里谈论它。 @Stomp 如果 k 是常数,则表的大小无关紧要。这就是我要说的。

以上是关于我可以做些啥来提高 Lua 程序的性能?的主要内容,如果未能解决你的问题,请参考以下文章

我正在运行一个 REST API 服务器,但我不太确定基础架构。另外,我可以做些啥来基准测试和提高速度?

我可以做些啥来存储类的实例并在反应应用程序的上下文中管理状态

在尝试将我的 iOS 应用程序提交到 App Store 时,我可以做些啥来克服下面列出的多个错误?

IBM Mainframe Assembler 程序在添加 COBOL 调用后显示 CPU 时间和运行时间的极端跳跃。为啥?可以做些啥来加快速度?

我应该做些啥来让它显示变量而不是值?

我必须做些啥来解决我的项目的“处理器架构之间的不匹配”?