【问题标题】:Does Lua optimize the ".." operator?Lua 是否优化了“..”运算符?
【发布时间】:2013-10-08 22:59:35
【问题描述】:

我必须执行以下代码:

local filename = dir .. "/" .. base

循环数千次(这是一个打印目录树的递归)。

现在,我想知道 Lua 是否一次性连接了 3 个字符串(dir、“/”、base)(即,通过分配一个足够长的字符串来保存它们的总长度),或者它是否这样做是低效的内部分两步:

local filename = (dir .. "/")              -- step1
                               .. base     -- step2

最后一种方式在内存方面效率低下,因为分配了两个字符串而不是一个。

我不太关心 CPU 周期:我主要关心内存消耗。

最后,让我概括一下这个问题:

Lua 在执行下面的代码时是只分配一个字符串,还是 4 个?

local result = str1 .. str2 .. str3 .. str4 .. str5

顺便说一句,我知道我可以做到:

local filename = string.format("%s/%s", dir, base)

但我尚未对其进行基准测试(内存和 CPU 方面)。

(顺便说一句,我知道 table:concat()。这会增加创建表的开销,所以我想它不会在所有用例中都有用。)

一个额外的问题:

如果 Lua 没有优化 ".." 运算符,定义一个用于连接字符串的 C 函数是否是个好主意,例如utils.concat(dir, "/", base, ".", extension)?

【问题讨论】:

  • 如果内存是您唯一关心的问题,请不要担心。这只是一个暂时的峰值,在第二次连接完成后,临时字符串将被快速收集。
  • table.concat{dir, "/", base, ".", extension}
  • @Eric 你确定我们可以在字符串上使用 table.concat 吗?
  • @SianLerkLau:仔细看——那些是大括号。 Lua 允许您跳过某些函数调用的括号

标签: c string performance lua


【解决方案1】:

尽管 Lua 对 .. 的使用进行了简单的优化,但您仍应小心在紧密循环中使用它,尤其是在连接非常大的字符串时,因为这会产生大量垃圾,从而影响性能。

连接多个字符串的最佳方法是使用table.concat

table.concat 允许您将表用作所有要连接的字符串的临时缓冲区,并且仅在您完成将字符串添加到缓冲区后才执行连接,如下面的愚蠢示例:

local buf = {}
for i = 1, 10000 do
    buf[#buf+1] = get_a_string_from_somewhere()
end
local final_string = table.concat( buf )

..的简单优化可以看分析以下脚本的反汇编字节码:

-- file "lua_06.lua"

local a = "hello"
local b = "cruel"
local c = "world"

local z = a .. " " .. b .. " " .. c

print(z)

luac -l -p lua_06.lua 的输出如下(对于 Lua 5.2.2):

主要(003E40A0 处的 13 条指令) 0+ 参数,8 个插槽,1 个上值,4 个局部变量,5 个常量,0 个函数 1 [3] 加载K 0 -1 ; “你好” 2 [4] 加载K 1 -2 ; “残忍的” 3 [5] 加载K 2 -3 ; “世界” 4 [7] 移动 3 0 5 [7] 加载K 4 -4 ; " " 6 [7] 移动 5 1 7 [7] 加载K 6 -4 ; " " 8 [7] 移动 7 2 9 [7] 连接 3 3 7 10 [9] 获取 4 0 -5 ; _ENV“打印” 11 [9] 移动 5 3 12 [9] 呼叫 4 2 1 13 [9] 返回 0 1

您可以看到只生成了一个CONCAT 操作码,尽管脚本中使用了许多.. 运算符。


要完全理解何时使用table.concat,您必须知道 Lua 字符串是不可变的。这意味着每当您尝试连接两个字符串时,您确实是在创建一个新字符串(除非结果字符串已经被解释器拦截,但这通常不太可能)。例如,考虑以下片段:

local s = s .. "hello"

并假设s 已经包含一个巨大的字符串(比如10MB)。执行该语句会创建一个新字符串(10MB + 5 个字符)并丢弃旧字符串。所以你刚刚为垃圾收集器创建了一个 10MB 的死对象来处理。如果你反复这样做,你最终会占用垃圾收集器。这是.. 的真正问题,这是典型的用例,需要收集表中最终字符串的所有部分并在其上使用table.concat:这不会避免产生垃圾(调用table.concat 后所有的碎片都会变成垃圾),但是你会大大减少不必要的垃圾。


结论

  • 在连接几个(可能很短)字符串时使用..,或者您没有处于紧密循环中。在这种情况下,table.concat 可能会给您更差的性能,因为:
    • 您必须创建一个表(通常您会丢弃);
    • 您必须调用函数table.concat(函数调用开销对性能的影响比使用内置的.. 操作符多次)。
  • 使用table.concat,如果您需要连接多个字符串,尤其是满足以下一个或多个条件时:
    • 您必须在后续步骤中执行此操作(.. 优化仅在同一表达式内起作用);
    • 你陷入了困境;
    • 字符串很大(例如,几 kB 或更多)。

请注意,这些只是经验法则。在性能非常重要的地方,您应该分析您的代码。

无论如何,Lua 在处理字符串时与其他脚本语言相比是相当快的,所以通常你不需要太在意。

【讨论】:

  • 分析字节码的好答案,我只考虑了 C 源代码。
  • @YuHao:我正要问你是否曾经生成过多参数 concat 操作码来使用该函数,但这很好地回答了这个问题
  • @YuHao 谢谢!我也尝试浏览源代码,但你在这方面打败了我 ;-)
  • @Lorenzo Donati:非常感谢您的出色回答。操作码输出显示“..”运算符在我的情况下是理想的。
【解决方案2】:

在您的示例中,.. 运算符是否进行优化对于性能而言几乎不是问题,您不必担心内存或 CPU。还有table.concat 用于连接许多字符串。 (见Programming in Luatable.concat的使用。

回到你的问题,在这段代码中

local result = str1 .. str2 .. str3 .. str4 .. str5

Lua 只分配一个新字符串,从 Lua 的相关源代码luaV_concat 中查看这个循环:

do {  /* concat all strings */
    size_t l = tsvalue(top-i)->len;
    memcpy(buffer+tl, svalue(top-i), l * sizeof(char));
    tl += l;
} while (--i > 0);
setsvalue2s(L, top-n, luaS_newlstr(L, buffer, tl));
total -= n-1;  /* got 'n' strings to create 1 new */
L->top -= n-1;  /* popped 'n' strings and pushed one */

可以看到 Lua 在这个循环中连接了n 字符串,但最后只推回了一个字符串,也就是结果字符串。

【讨论】:

    猜你喜欢
    • 2019-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-09
    • 2015-09-26
    • 1970-01-01
    相关资源
    最近更新 更多