C语言 Lua是否优化了..“”运算符?

2exbekwf  于 2023-01-04  发布在  其他
关注(0)|答案(3)|浏览(148)

我必须执行以下代码:

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明智的)。
(BTW,我知道table:concat(),这会增加创建表的开销,所以我想它不会在所有用例中都有好处。)
额外问题:
如果Lua没有优化“..”操作符,那么定义一个C函数来连接字符串(例如utils.concat(dir, "/", base, ".", extension))是否是个好主意?

moiiocjp

moiiocjp1#

尽管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-* 编辑:Lua 5.3.6 * 中也输出相同字节码):

main  (13 instructions at 003E40A0)
0+ params, 8 slots, 1 upvalue, 4 locals, 5 constants, 0 functions
    1   [3] LOADK       0 -1    ; "hello"
    2   [4] LOADK       1 -2    ; "cruel"
    3   [5] LOADK       2 -3    ; "world"
    4   [7] MOVE        3 0
    5   [7] LOADK       4 -4    ; " "
    6   [7] MOVE        5 1
    7   [7] LOADK       6 -4    ; " "
    8   [7] MOVE        7 2
    9   [7] CONCAT      3 3 7
    10  [9] GETTABUP    4 0 -5  ; _ENV "print"
    11  [9] MOVE        5 3
    12  [9] CALL        4 2 1
    13  [9] RETURN      0 1

您可以看到,虽然脚本中使用了许多..操作符,但只生成了一个CONCAT操作码。
要完全理解什么时候使用table.concat,你必须知道Lua字符串是 * 不可变的 *,这意味着当你试图连接两个字符串时,你实际上是在创建一个新的字符串(除非结果字符串已经被解释器接收,但这通常是不可能的)。

local s = s .. "hello"

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

结论

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

注意,这些只是经验法则,在性能真正重要的地方,你应该分析你的代码。
无论如何,与其他脚本语言相比,Lua在处理字符串时速度相当快,所以通常不需要太在意。

gudnpqoy

gudnpqoy2#

在您的示例中,..操作符是否进行优化对性能来说几乎不是问题,您不必担心内存或CPU。还有table.concat用于连接许多字符串。(参见Programming in Lua)了解table.concat的用法。
回到你的问题在这段代码中

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

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

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字符串,但最后只将一个字符串(即结果字符串)推回堆栈。

jgzswidk

jgzswidk3#

顺便说一句,我知道table:concat(),这会增加创建表的开销,所以我想它不会对所有用例都有好处。
在这个特定的用例(以及类似的用例)中,如果您担心创建大量垃圾表,可以考虑重用表:

local path = {}
...
-- someplace else, in a loop or function:
path[1], path[2] = dir, base
local filename = table.concat(path, "/")
path[1], path[2] = nil
...

你甚至可以把它归纳为一个“concat”实用程序:

local rope = {}
function string_concat(...)
    for i = 1, select("#", ...) do rope[i] = select(i, ...) end -- prepare rope
    local res = table.concat(rope)
    for i = 1, select("#", ...) do rope[i] = nil end -- clear rope
    return res
end

相关问题