HALion script/Lua optimization tips

I figured we could compile a list of optimization tips that people may find helpful. Are there optimization tricks that anyone have found helpful? I’ve looked up Lua optimization tips but I am unsure if these tips help in regards to audio work, especially specific to HALion. I’ve read things such as using local variables over global variables. But what happens when you are using the same variable hundreds of times. At that point is the hundreds of local variables more taxing than the one global? Or what if the global variable is an external local variable? Making strings and tables external of for loops makes sense to speed computation time but what about things that are HALion specific?

For example, is it more efficient to convert a parameter’s value to 1 using setParamterNormalized or getParameterNormalized and then perform math in relation to the value of that parameter being between 0 and 1? Or, is more efficient to just perform a math operation directly the value of the parameter?

Is it more efficient to build a mixed table and create a parameter with a function to pull from the values of the table to control HALion? Or, is it more efficient to create parameter and define it as a string. Then to use a Boolean to control HALion based on the value of the string?

These are just a couple examples but the point is, what can we do to make our code work the fastest and load the quickest? What have you tried that has given better results?

Interesting topic. You probably need to ask Steinberg developers if you want a definitive answer.

Here are my thoughts about this but I am be wrong.

The local variable trick seems to work. You can try it by creating a table with lot of entries (1000 or more). Looping through the table should be quicker if the table was local.

Things like getZone, getLayer… Creating a variable and referring to it instead of calling the function repeatedly should be more efficient.

Does it make a difference? That’s a difficult questions. In lot of cases you probably won’t notice any difference.

So I did a quick test by changing all of my global variables to local external variables and then loading a library preset. My preset took .3 seconds longer to load with the external local variables. I wonder how this would effect the actual functionality of the script when things are being automated or when multiple parameters are being adjusted simultaneously.

Creating a variable for getZone, getLayer is typically how I reference individual modules. I do it more so because it cuts down on the amount of things I need to write and because it allows me to change multiple parameters at once or spot an issue if a single parameter isn’t doing what I intended (since they all have the same reference). The possibility that this makes more optimized code was not my original intention although it makes sense that it would work better.

Another thing to note is that if a variable references a table then that table should only go into memory the one time. Any time it is called into scope it should just reference the same table as opposed to creating a new copy of the table in memory. So in theory that should mean that doing something as simple as x = {“TIME”,“BPM”}, and then defining any string parameters in your script with just x in theory should be more efficient. But like you said, I’m not sure how well that translate into HALion. I kind of wish the developer manual talked a little about optimization.

This is an interesting observation. So it looks like it’s not that simple in the end.

Another question would be if it is more efficient to have multiple functions for multiple parameters or one function for many parameters?
For example is this…

function onLFO1Change()
  setParameter(5, LFO1)
end
function onLFO2Change()
  setParameter(5, LFO2)
end
function onLFO3Change()
  setParameter(5, LFO3)
end

defineParameter{
  name = "LFO1",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFO1Change
  }

defineParameter{
  name = "LFO2",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFO2Change
}

defineParameter{
  name = "LFO3",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFO3Change
}

more efficient than this?

function onLFOChange()
  setParameter(5, LFO1)
  setParameter(5, LFO2)
  setParameter(5, LFO3)
end

defineParameter{
  name = "LFO1",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFOChange
  }

defineParameter{
  name = "LFO2",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFOChange
}

defineParameter{
  name = "LFO3",
  default = 0,
  min = 0,
  max = 100,
  increment = 1,
  onChanged = onLFOChange
}

I ask this sort of question because updating multiple parameters with no value change is probably expensive but so is calling individual functions.

@abject39

Can you try these scripts? For tables… slot local seems to give best performance. What’s really strange that controller thread seems to be faster than processor thread. It should be the other way around.

gt = {}
for i = 1, 1000 do
    gt[i] = i
end

local lt = {}
for i = 1, 1000 do
    lt[i] = i
end

defineSlotLocal("slt")
slt = {}
for i = 1, 1000 do
    slt[i] = i
end


function loopTable(tb)
    local t1 = getTime()
    for i = 1, #tb do
        tb[i] = tb[i] + 1
    end
    local t2 = getTime()
    print("Thread: ", getContext())
    return t2 - t1
end

print("Global table: ", loopTable(gt), " ms")
print("Local table: ", loopTable(lt), " ms")
print("Slot local table: ", loopTable(slt), " ms")

count = 0

function onNote(e)
    count = count + 1
    if count % 3 == 0 then     
        print("Global table: ", loopTable(gt), " ms")
    elseif count % 3 == 1 then
        print("Local table: ", loopTable(lt), " ms")
    else
        print("Slot local table: ", loopTable(slt), " ms")
    end
end

In this script local variable seems to be the fastest.

a = 0
local b = 0
defineSlotLocal("c")
c = 0
count = 0


function onNote(e)
    count = count + 1
    if count % 3 == 0 then
        local t1 = getTime()
        for i = 1, 1000 do
            a = a + 1
        end
        local t2 = getTime()
        print("Global: ", t2 - t1, " ms")
    elseif count % 3 == 1 then
        local t1 = getTime()
        for i = 1, 1000 do
            b = b + 1
        end
        local t2 = getTime()
        print("Local: ", t2 - t1, " ms")
    else
        local t1 = getTime()
        for i = 1, 1000 do
            c = c + 1
        end
        local t2 = getTime()
        print("Slot local: ", t2 - t1, " ms")
    end
end
1 Like

I can confirm the same results although it was not always consistent. It seems the calculations are overall performed extremely fast with the differences being negligible. But, part of me assumes things can become a lot more complex and taxing when multiple instruments are playing in real time and being automated. The real question is how does this effect initializing the preset/script? I wonder how this effects load time. I wonder if we can measure initial onload time. Maybe it is the macro page and sample/wavetables that have the most effect on load times and not the number of parameters created within the script.

You could define your parameters with processor callback. This has been added with some of the recent updates.

You can try to put the getTime inside the onInit callback to see what you get.

Or take a bit less scientific approach. Measure the load time of your preset. Then delete the macro page and measure again. Then also delete the script so you are left with samples only.

I recall a post saying the ui script took extremely long to load. Don’t know if it has been fixed yet.

So I tried a bit of the less scientific approach. I deleted the macro page bitmaps and reloaded the same preset. It seemed to load no faster at all. I didn’t measure with any sort of code or timer. This was just my perception from loading it and looking. I believe this means that the number of UI elements that make up the macro page have a large effect on load time, not just the size of the bitmaps used in it.