Search code examples
cstringperformancelua

Does Lua optimize the ".." operator?


I have to execute the following code:

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

thousands of times in a loop (it's a recursion that prints a directory tree).

Now, I wonder whether Lua concatenates the 3 strings (dir, "/", base) in one go (i.e., by allocating a string long enough to hold their total lengths) or whether it does this the inefficient way by doing it internally in two steps:

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

This last way would be inefficient memory-wise because two strings are allocated instead of just one.

I don't care much about CPU cycles: I care mainly about memory consumption.

Finally, let me generalize the question:

Does Lua allocate only one string, or 4, when it executes the following code?

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

BTW, I know that I could do:

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

But I've yet to benchmark it (memory & CPU wise).

(BTW, I know about table:concat(). This has the added overhead of creating a table so I guess it won't be beneficial in all use cases.)

A bonus question:

In case Lua doesn't optimize the ".." operator, would it be a good idea to define a C function for concatenating strings, e.g. utils.concat(dir, "/", base, ".", extension)?


Solution

  • Although Lua performs a simple optimization on .. usage, you should still be careful to use it in a tight loop, especially when joining very large strings, because this will create lots of garbage and thus impact performance.

    The best way to concatenate many strings is with table.concat.

    table.concat lets you use a table as a temporary buffer for all the strings to be concatenated and perform the concatenation only when you are done adding strings to the buffer, like in the following silly example:

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

    The simple optimization for .. can be seen analyzing the disassembled bytecode of the following script:

    -- file "lua_06.lua"
    
    local a = "hello"
    local b = "cruel"
    local c = "world"
    
    local z = a .. " " .. b .. " " .. c
    
    print(z)
    

    the output of luac -l -p lua_06.lua is the following (for Lua 5.2.2 - edit: the same bytecode is output also in 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
    

    You can see that only a single CONCAT opcode is generated, although many .. operators are used in the script.


    To fully understand when to use table.concat you must know that Lua strings are immutable. This means that whenever you try to concatenate two strings you are indeed creating a new string (unless the resulting string is already interned by the interpreter, but this is usually unlikely). For example, consider the following fragment:

    local s = s .. "hello"
    

    and assume that s already contains a huge string (say, 10MB). Executing that statement creates a new string (10MB + 5 characters) and discards the old one. So you have just created a 10MB dead object for the garbage collector to cope with. If you do this repeatedly you end up hogging the garbage collector. This is the real problem with .. and this is the typical use case where it is necessary to collect all the pieces of the final string in a table and to use table.concat on it: this won't avoid the generation of garbage (all the pieces will be garbage after the call to table.concat), but you will greatly reduce unnecessary garbage.


    Conclusions

    • Use .. whenever you concatenate few, possibly short, strings, or you are not in a tight loop. In this case table.concat could give you worse performance because:
    • you must create a table (which usually you would throw away);
    • you have to call the function table.concat (the function call overhead impacts performance more than using the built-in .. operator a few times).
    • Use table.concat, if you need to concatenate many strings, especially if one or more of the following conditions are met:
    • you must do it in subsequent steps (the .. optimization works only inside the same expression);
    • you are in a tight loop;
    • the strings are large (say, several kBs or more).

    Note that these are just rules of thumb. Where performance is really paramount you should profile your code.

    Anyway Lua is quite fast compared with other scripting languages when dealing with strings, so usually you don't need to care so much.