Lua performance optimization skills (V): reduction, reuse and recycling

Time:2022-4-29

When dealing with Lua resources, we should also follow the 3R principle advocated for Earth Resources – reduce, reuse and recycle, that is, reduce, reuse and recycle.

Cutting is the easiest way. There are many ways to avoid using new objects. For example, if your program uses too many tables, you can consider changing the representation of data. For the simplest example, suppose your program needs to operate polylines. The most natural expression is:

 

Copy codeThe code is as follows:

polyline =
{
    { x = 10.3, y = 98.5 },
    { x = 10.3, y = 18.3 },
    { x = 15.0, y = 98.5 },
    –…
}


Although it is natural, this form of expression is not economical for large-scale broken lines, because each point of it needs to be described by a table. The first alternative is to use arrays to record, which can save some memory:

 

 

Copy codeThe code is as follows:

polyline =
{
     { 10.3, 98.5 },
     { 10.3, 18.3 },
     { 15.0, 98.5 },
     –…
}


For a polyline with one million points, this modification can reduce the memory footprint from 95kb to 65kb. Of course, you have to pay for readability: P [i] X is easier to understand than p [i] [1].

 

Another more economical approach is to use one array to store all x coordinates and another to store all y coordinates:

Copy codeThe code is as follows:

polyline =
{
    x = { 10.3, 10.3, 15.0, …},
    y = { 98.5, 18.3, 98.5, …}
}


Original

Copy codeThe code is as follows:

p[i].x


Now it’s

Copy codeThe code is as follows:

p.x[i]


Using this expression, the memory footprint of a one million point polyline is reduced to 24Kb.

 

Recycling is a good place to look for opportunities to reduce the number of garbage collections. For example, if you create an unchanged table in a loop, you can move it outside the loop or even outside the function as the upper value. Trial comparison:

 

Copy codeThe code is as follows:

function foo (…)
     for i = 1, n do
          local t = {1, 2, 3, “hi”}
— do something that won’t change the T table
          –…
     end
end


and

Copy codeThe code is as follows:

Local T = {1, 2, 3, “Hi”} — create t once and for all
function foo (…)
    for i = 1, n do
— do something that won’t change the T table
        –…
    end
end


The same technique can also be used for closures, as long as you don’t move them out of the scope where they are needed. For example, the following function:

 

 

Copy codeThe code is as follows:

function changenumbers (limit, delta)
    for line in io.lines() do
        line = string.gsub(line, “%d+”, function (num)
            num = tonumber(num)
            if num >= limit then return tostring(num + delta) end
— otherwise, no value will be returned and the original value will be maintained
        end)
        io.write(line, “\n”)
    end
end


We can avoid creating a new closure for each iteration by moving the internal function outside the loop:

 

 

Copy codeThe code is as follows:

function changenumbers (limit, delta)
    local function aux (num)
        num = tonumber(num)
        if num >= limit then return tostring(num + delta) end
    end
    for line in io.lines() do
        line = string.gsub(line, “%d+”, aux)
        io.write(line, “\n”)
    end
end


However, we cannot move aux out of the changenumbers function because aux needs to access limit and delta.

 

For a variety of string processing, we can reduce the need to create new strings by using the index of existing strings. For example, string The find function returns the location index where it finds the specified pattern, not the matching string. By returning the index, it avoids creating new strings when a successful match occurs. When necessary, programmers can call string Get substring of string to match [1].

When we cannot avoid using new objects, we can still avoid creating new objects by reusing them. For strings, reuse is not necessary, because Lua has done such work for us: it always internalizes all used strings and reuses them whenever possible. However, for tables, reuse can be very effective. As a general example, let’s go back to the case of creating tables in loops. This time, the contents of the table are no longer unchanged. Generally, we can reuse this table in all iterations, simply changing its content. Consider the following code snippet:

Copy codeThe code is as follows:

local t = {}
for i = 1970, 2000 do
    t[i] = os.time({year = i, month = 6, day = 14})
end


The following code is equivalent, but reuses this table:

Copy codeThe code is as follows:

local t = {}
local aux = {year = nil, month = 6, day = 14}
for i = 1970, 2000 do
    aux.year = i
    t[i] = os.time(aux)
end


A particularly effective way to achieve reuse is caching [2]. The basic idea is very simple. Store the calculation results corresponding to the specified input. The next time you accept the same input again, the program only needs to simply reuse the last calculation results.

 

LPEG, Lua’s new pattern matching library, uses an interesting caching process. LPEG compiles each pattern string into an internal applet for matching strings. Compared with the matching itself, this compilation process is very expensive. Therefore, LPEG caches the compilation results for reuse. Just a simple table, with the mode string as the key and the compiled applet as the value for recording.

A common problem when using caching is that the memory overhead caused by storing calculation results is greater than the performance improvement caused by reuse. To solve this problem, we can use a weak table in Lua to record the calculation results, so the unused results will eventually be recycled.

In Lua, using higher-order functions, we can define a general caching function:

 

Copy codeThe code is as follows:

function memoize (f)
Local MEM = {} — cached table
Setmetatable (MEM, {_mode = “kV”}) — set as weak table
Return function (x) — the new version of ‘f’ after caching
        local r = mem[x]
If r = = nil then — no previously recorded results?
R = f (x) — call the original function
MEM [x] = R — store results for reuse
        end
        return r
    end
end


For any function f, memoize (f) returns the same return value as F, but caches it. For example, we can redefine loadstring as a cached version:

 

loadstring = memoize(loadstring)
The new function is used in exactly the same way as the old one, but if there are many duplicate strings during loading, the performance will be greatly improved.

If your program creates and deletes too many coroutines, recycling may improve its performance. The existing collaboration API does not directly provide support for reusing collaboration, but we can try to bypass this limitation. For the following processes:

Copy codeThe code is as follows:

co = coroutine.create(function (f)
    while f do
        f = coroutine.yield(f())
    end
end)


This coroutine accepts a job (runs a function), executes it, and waits for the next job when it is completed.

 

Most of the recycling in Lua is done automatically through the garbage collector. Lua uses a progressive garbage collector, which means that the garbage collection work will be divided into many small steps, which will be executed (gradually) as the program allows. The gradual rhythm is proportional to the speed of memory allocation. Whenever a certain amount of memory is allocated, the corresponding memory will be recovered in proportion; The faster a program consumes memory, the faster the garbage collector attempts to reclaim memory.

If we follow the principles of reduction and reuse when writing programs, usually the garbage collector won’t have much to do. But sometimes we can’t avoid making a lot of garbage, and the work of garbage collector will become very heavy. The garbage collector in Lua is adjusted to fit the average program, so it works well in most programs. However, at a specific time, we can adjust the garbage collector to obtain better performance. By calling the function collectgarbage in Lua or Lua in C_ GC to control the garbage collector. They have the same function, but have different interfaces. In this case, I will use the Lua interface, but this operation is usually better in C.

The collectgarbage function provides several functions: it can stop or start the garbage collector, force a complete garbage collection, obtain the total memory occupied by Lua, or modify two parameters that affect the working rhythm of the garbage collector. They have their own uses when adjusting programs with high memory consumption.

Stopping the garbage collector “forever” may be useful for some batch programs. These programs create several data structures, produce some output values based on them, and then exit (such as compiler). For such programs, trying to recycle garbage will be a waste of time, because the amount of garbage is small, and the memory will be completely released after the program is executed.

For non batch programs, stopping the garbage collector is not a good idea. However, these programs can pause the garbage collector during some extremely time sensitive periods to improve time performance. If necessary, these programs can obtain full control of the garbage collector, keep it in a stopped state, and only explicitly perform a forced step or complete garbage collection at a specific time. For example, many event driven platforms provide an option to set idle functions to be called when there are no messages to be processed. This is a good time to call garbage collection (in Lua 5.1, whenever you perform a forced collection when the garbage collector is stopped, it will resume operation. Therefore, if you want to keep the garbage collector stopped, you must call collectgarbage (“stop”) immediately after the forced Collection).

Finally, you may want to adjust the parameters of the collector. The garbage collector has two parameters to control its rhythm: the first is called pause time, which controls how long the collector has to wait after completing a collection and before starting the next collection; The second parameter, called step coefficient, controls how much content the recycler recycles in each step. Roughly speaking, the smaller the pause time and the larger the step coefficient, the faster the garbage collection. The impact of these parameters on the overall performance of the program is difficult to predict. A faster garbage collector will obviously waste more CPU cycles, but it will reduce the total memory consumption of the program and may reduce paging. Only careful testing can give you the best parameter value.

[1] If the standard library provides a function to compare two substrings, it may be a good idea, so that we can check the specific value in the string without solving the substring (a new string will be created).

[2] Cache

Recommended Today

Tutorial on sending e-mail using net:: SMTP class in Ruby

Simple Mail Transfer Protocol(SMTP)SendE-mailAnd routing protocol processing between e-mail servers. RubyIt provides the connection of simple mail transfer protocol (SMTP) client of net:: SMTP class, and provides two new methods: new and start New takes two parameters: Server name defaults to localhost Port number defaults to 25 The start method takes these parameters: Server – […]