Оптимизирует ли Lua оператор ".."?

Мне нужно выполнить следующий код:

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

тысячи раз в цикле (это рекурсия, которая печатает дерево каталогов).

Теперь я задаюсь вопросом, объединяет ли Lua 3 строки (dir, "/", base) за один раз (т.е. выделяя строку, достаточно длинную, чтобы удерживать их общую длину), или делает это неэффективным способом, делая это внутренне в два этапа:

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

Этот последний способ будет неэффективным по памяти, потому что вместо строки выделяются две строки.

Мне очень не нравятся циклы процессора: я в основном забочусь о потреблении памяти.

Наконец, позвольте мне обобщить вопрос:

Lua выделяет только одну строку или 4, когда она выполняет следующий код?

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

Кстати, я знаю, что могу сделать:

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

Но мне еще предстоит сравнить его (память и процессор).

(Кстати, я знаю о таблице: concat(). У этого есть дополнительные накладные расходы на создание таблицы, поэтому я думаю, что это не будет полезно во всех случаях использования.)

Бонусный вопрос:

В случае, если Lua не оптимизирует оператор "..", было бы неплохо определить функцию C для конкатенации строк, например. utils.concat(dir, "/", base, ".", extension)?

Ответы

Ответ 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 )

Простую оптимизацию для .. можно увидеть, анализируя дизассемблированный байт-код следующего script:

-- 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):

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, хотя в script используются многие операторы ...


Чтобы полностью понять, когда использовать table.concat, вы должны знать, что строки Lua неизменяемы. Это означает, что всякий раз, когда вы пытаетесь объединить две строки, вы действительно создаете новую строку (если результирующая строка уже интернирована интерпретатором, но это обычно маловероятно). Например, рассмотрим следующий фрагмент:

local s = s .. "hello"

и предположим, что s уже содержит огромную строку (например, 10 МБ). Выполнение этого оператора создает новую строку (10 Мбайт + 5 символов) и отбрасывает старую. Таким образом, вы только что создали мертвый объект размером 10 МБ для сборщика мусора. Если вы это сделаете повторно, вы в конечном итоге засоряете сборщик мусора. Это реальная проблема с .., и это типичный вариант использования, когда необходимо собрать все части финальной строки в таблице и использовать table.concat на ней: это не позволит избежать образования мусора (все куски будут мусором после вызова table.concat), но вы значительно уменьшите ненужный мусор.


Выводы

  • Используйте .. всякий раз, когда вы объединяете несколько, возможно, коротких строк, или вы не находитесь в узком цикле. В этом случае table.concat может дать вам худшую производительность, потому что:
    • вы должны создать таблицу (которая обычно выбрасывается);
    • вам нужно вызвать функцию table.concat (служебные данные вызова функции влияют на производительность больше, чем использование встроенного оператора .. несколько раз).
  • Используйте table.concat, если вам нужно объединить многие строки, особенно если выполнено одно или несколько из следующих условий:
    • вы должны сделать это на последующих шагах (оптимизация .. работает только внутри одного выражения);
    • вы находитесь в узком цикле;
    • строки большие (скажем, несколько кБ или более).

Обратите внимание, что это просто эмпирические правила. Там, где производительность действительно важна, вы должны профилировать свой код.

В любом случае Lua довольно быстро сравнивается с другими языками сценариев при работе со строками, поэтому обычно вам не нужно так много заботиться.

Ответ 2

В вашем примере, если оптимизатор .. делает оптимизацию едва ли проблемой для производительности, вам не нужно беспокоиться о памяти или процессоре. И там 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 */

Вы можете видеть, что строки Lua concatenate n в этом цикле, но только возвращают в стек одну строку в конце, которая является строкой результата.