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.

@Chris.StAubyn

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.

So I tried to change a parameter to use a processor callback and got an error saying that the function could not be used in the processor thread because it was a controller thread function. If the same function that would normally be used in either thread (cannot be changed to the other). What’s the point of having the option of changing the callback type?

Also, my experience has taught me that combining as many GUI assets as possible is always more efficient. This is especially true if it is something in the interface that doesn’t actually move. Even if it doesn’t lower the load time it can lower the file size. Here’s an example. Let’s say you have a circular knob with no particularly notable features. Instead of drawing a set of frames for each knob position, draw a frame for each position of the knob’s maker. Then just draw the rest of the knob on the background image. Since a rotating knob is the same circle optically, the only part that needs to move is the marker. This may not load faster than a single knob, but when you multiply this across multiple different knobs in an interface it becomes more efficient. This is because you only need one set of markers to use for all knobs and one background (since the background portion of the knobs can have all different styles/colors of knobs), as opposed to loading multiple knobs in multiple styles and colors. This takes a lot pixel precision to pull off but this can result in a more efficient interface with a lower file size.

Did you define the parameter with named arguments? Just quickly tried and it seems to work ok here.

zone = this.parent:getZone()

function zoneTypeChanged()
	zone:setParameter("ZoneType", zoneType)
	print(getContext())
end

defineParameter{
	name = "zoneType",
	default = 0,
	strings = {[0] = "Synth", "Sample", "Grain"},
	onChanged = zoneTypeChanged,
	processorCallback = true
	}

True… I learned this the hard way. I was collaborating on one project which took 8 seconds to load. A lot of this was caused by macro page resources. Each element had a separate png file so there was about 20 files. And they were all very high resolution. Switch that has a size of 100 * 100 pixels on macro page had a resource png file of 2500 * 25000 pixels.

Yeah, If I remember correctly, I attempted to use it on a function that constructed an effect.

Wow! 8 seconds is an eternity these days. Making use of Sections is critical to getting that load time down. Steinberg has removed the documentation but there is a limitation in pixels per bitmap which causes assets not to load on some computers. I can’t remember the exact dimensions but it’s around 1000x8000. I’ve found that you can get more resolution and keep within standards by chopping up the frames horizontally. For example, if you have a knob that is 100x100 and you want 200 frames you will and up with a 100x20000 bitmap. This could cause issues when loading on some computers along with being overall inefficient. A workaround is to cut each frame into quarters. So you end up with 100x25 individual frames. Now you’ll have 4 sets of bitmaps that are 100x2500. This is much more manageable for HALion. You can put them on the same bitmap horizontally which would result in a 400x2500 bitmap. You then create Sections out of them and line up 4 knobs together on your macro page and they will have the illusion of one single knob.

1 Like

Side note: I think it’s safe to say that the interface can have the most profound effect on what the end-user perceives as inefficient. Only twice, thus far, have I had code that performed “too slow”. Both times were correctable. One occurred when I added a incremental switch, which forced the code to perform fast if the switch was held down. The other use a construct function which had to remove and create an element along with getting previous values and setting new values to the previous ones. Doing what we mentioned here sped up both (slot local).

1 Like