Search code examples
luaswitch-statementlua-table

How can I optimize these elseifs?


I'm working on a leveling and experience system in lua and I was wondering how could I optimize and maybe transform these elseifs in a lua table. The idea is to check if mylevel is less or equal to a defined maxlevel to use different multipliers depending on your level

local mylevel = 1
local experience = 50

if mylevel and experience then
    if mylevel <= 10 then
        multiplier = 1
    elseif mylevel <= 15 then
        multiplier = 2
    elseif mylevel <= 25 then
        multiplier = 3
    elseif mylevel <= 35 then
        multiplier = 4
    elseif mylevel <= 45 then
        multiplier = 5
    elseif mylevel <= 80 then
        multiplier = 6
    elseif mylevel <= 100 then
        multiplier = 7
    elseif mylevel > 100 then
        multiplier = 8
    end

end

Solution

  • When to optimize

    First off: Do you really need to optimize? First benchmark and verify whether there is a performance issue. Often asymptotically suboptimal constructs are in practice optimal or at least "good enough" for small numbers due to overhead. Given your small elseif chain of about ten branches, this might very well be the case. I'd also imagine that (at least in simpler cases) LuaJIT might be able to perform optimizations to achieve sublinear asymptotic runtime (f.E. I'd expect a switch-like construct with many equality comparisons to become something like a jump table when the code is JIT compiled; it probably won't nest comparisons intelligently though, esp. since there can be a metamethod involved and order matters). There probably is no need for optimization here.

    If you want to switch to a data-driven design for maintenance reasons, that's a different matter. But even there it might be argued that the code in its current form is straightforward and readable.

    Bottom line: I see no need to touch your code (yet). You can very well keep it simple as-is; only if you are to add (many) more levels is when you should consider "proper" data structures.

    Note also that you can replace your last elseif with simply an else, assuming <= is sane.

    How to optimize

    Binary Search

    InSync's answer

    Gets the job done in asymptoptically logarithmic time. Has significant overhead though.

    An improved version

    It is indeed preferable to store levels and their corresponding multipliers out of band:

    local levels = {10, 15, 25, 35, 45, 80, 100}
    local multipliers = {1, 2, 3, 4, 5, 6, 7, 8}
    

    then binary search on levels and use the returned insertion index to find the corresponding multiplier.

    The reduced overhead is twofold:

    • Assumedly less memory consumption: Tables in Lua are heavy; they need to keep track of the length of their hash and list part as well as a possible metatable. Using tables for simple pairs is highly memory inefficient.
    • Assumedly better performance: This eliminates the indirection of having to first look up a pointer to the pair, then having to look up the first element of that pair on the heap. It should also improve the cache locality during the last few steps of the binary search, since nearby values may already be in the cache.

    Code is data, data is code

    I conjure that it would be most efficient to express the binary search directly through nested ifs. That way you get rid of just about any and all indirection.

    This is how may write it given your if-elseif-else chain:

    if mylevel <= 35 then
        if mylevel <= 15 then
            if mylevel <= 10 then
                multiplier = 1
            else
                multiplier = 2
            end
        else
            if mylevel <= 25 then
               multiplier = 3
            else
               multiplier = 4
        end
    else
        if mylevel <= 80 then
            if mylevel <= 45 then
                multiplier = 5
            else
                multiplier = 6
            end
        else
            if mylevel <= 100 then
               multiplier = 7
            else
               multiplier = 8
        end
    end
    

    This is of course not particulary maintainable: It is in fact hardcoding a (reasonably) balanced binary search tree into your code. You could however write yourself some code to generate this code.

    Lookup Array

    The alternative to a binary search is a simple table which maps from levels to multipliers. Since this is practically an array, access will be optimal - O(1). However you'll need to waste O(n) memory where n is the number of levels up to the upper bound after which you just return a constant max - 100 in your case. This might look as follows:

    local multipliers = {}
    local idx = 0
    local function add_multipliers(levels, mult)
        for _ = 1, levels do
            idx = idx + 1
            multipliers[idx] = mult
        end
    end
    add_multipliers(10, 1) -- first ten levels have multiplier 1, and so on...
    add_multipliers(5, 2) -- next five levels have 2
    add_multipliers(10, 3)
    add_multipliers(10, 4)
    add_multipliers(10, 5)
    add_multipliers(35, 6)
    add_multipliers(20, 7)
    

    now your function becomes trivial:

    local function get_multiplier(level)
        if level > 100 then return 8 end
        return multipliers[level]
    end
    

    or, as Nifim points out, simply

    local function get_multiplier(level)
        return multipliers[level] or 8
    end
    

    since multipliers[level] will be nil when level is out of bounds, thus 8 conceptually is a "default" value (small caveat: this might mask mistakes such as passing level = nil or level = 0; it would return 8 for those, whereas the previous solution would error or return nil)

    This is very likely to be both simpler and faster than a binary search. Use this if your numbers are small, as in your example.