19

我必须执行以下代码:

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)

4

2 回答 2

43

尽管 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 与其他脚本语言相比是相当快的,所以通常你不需要太在意。

于 2013-10-02T14:27:20.407 回答
9

在您的示例中,..运算符是否进行优化对于性能几乎不是问题,您不必担心内存或CPU。还有table.concat用于连接许多字符串。(请参阅Lua中的编程)以了解table.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 */

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

于 2013-10-02T14:46:52.360 回答