Slow down due accessing Renoise objects via table index discussion

Summary

If you change a vst parameter value via scripting, the operation seems to take a ton of time (macos). It should be instant.

Steps to reproduce:

  • Install EQHelper tool
  • Install Pro Q3 VST3
  • Draw any kind of EQ curve with a lot of bands, e.g. 24
  • Now open context menu on the dsp device and select “transpose +12” while keeping the plugins GUI opened. You literally can see how slow it changes the parameter values
  • Now undo and redo. It is now instant.

The change happens in a loop, but instantly in one and the same call, as far as I can see. Something fishy is going on here…

My bad, used devicePointer.parameters[index] instead devicePointer:parameter(index) . Forgot this, IMHO this should be used in the docs always, too. to avoid confusion.

So this actually is a slow down?

function findDeviceInTracks(device) 
  for _, track in pairs(sng.tracks) do
    for _, trackDevice in pairs(track.devices) do
      if (rawequal(device, trackDevice)) then
        return track
      end
    end
  end

  return nil
end

And should be something like:

function findDeviceInTracks(device) 
  for index = 1, #sng.tracks do
    for index2 = 1, #song:track(index).devices do
      if (rawequal(device, song:track(index):device(index2))) then
        return track
      end
    end
  end

  return nil
end

Using indexes for no reason (where it should return an object) makes the code really ugly… Is there a way to use the function instead the table index, but NOT using indices?

No.

The first is faster (pretty sure), because the track object is already localized, provided by the iterator.

In the second example you will make a lot of redundant track object creations.

Yeah ok, but that was not the point. So this would be faster?

function findDeviceInTracks(device) 
  for index = 1, #sng.tracks do
    local track = song:track(index)
    for index2 = 1, #track.devices do
      if (rawequal(device, track:device(index2))) then
        return track
      end
    end
  end

  return nil
end

Just as an example.

Summary
local time, song = os.clock(), renoise.song()

function findDeviceInTracksA(device) 
  for _, track in pairs(song.tracks) do
    for _, trackDevice in pairs(track.devices) do
      if (rawequal(device, trackDevice)) then
        return track
      end
    end
  end

  return nil
end

function findDeviceInTracksB(device) 
  for index = 1, #song.tracks do
    local track = song:track(index)
    for index2 = 1, #track.devices do
      if (rawequal(device, track:device(index2))) then
        return track
      end
    end
  end

  return nil
end

for i = 1, 10000 do
  findDeviceInTracksA(song:track(1):device(1))
--  findDeviceInTracksB(song:track(1):device(1))
end

print(os.clock()-time)

ipairs: 1.47s
no ipairs: 1.53s

2 Likes

Thanks, interesting. Also still very slow. Should be 10ms or so :grinning_face_with_smiling_eyes:

Btw, possibly noteworthy: Renoise tools - speed optimization initiative - #7 by joule

1 Like

@ffx @joule This example can be optimised even further…

My example function “findDeviceInTracksC” runs approx 3-4x faster than A and B.

It’s really all about knowing the weird quirks, I guess.

You just have to experiment a bit and see what works best. (Without driving yourself completely insane over CPU cycles…)

Either way… “Ugly” code or not… If it’s a critical function within your tool (ooeerr!) then its performance should be your main concern, imho, and it’s definitely worth the effort to dig deep… :slight_smile:

Cheers,
K.

-- Global song reference.
-- (As per the original post...) 
local g_song = renoise.song()


-- Method 1 : Pretty good! However... While the ipairs()/pairs()
-- iterators *are* pretty handy, they are not *super* fast...
function findDeviceInTracksA(device)
  for _, track in pairs(g_song.tracks) do
    for _, trackDevice in pairs(track.devices) do
      if (rawequal(device, trackDevice)) then
        return track
      end
    end
  end

  return nil
end


-- Method 2 : Not too bad, but...
-- Repeatedly accessing stuff via tables and looking up the same
-- objects over and over again is redundant and just wastes CPU
-- cycles... No bueno!
function findDeviceInTracksB(device)
  for index = 1, #g_song.tracks do
    local track = g_song:track(index)
    for index2 = 1, #track.devices do
      if (rawequal(device, track:device(index2))) then
        return track
      end
    end
  end

  return nil
end


-- Method 3 : Big pimpin...
-- Local references are best! Fetch them when your function actually
-- needs them, just before any major loops or heavy lifting, just 
-- to avoid any repeated unncessary calls later during other critical
-- processes...
function findDeviceInTracksC(device) 

  -- Local reference to the song...
  -- It is tempting to use a "global" song variable here, and then let
  -- all of your functions refer back to that, but once again... you
  -- should take care, because the song state *may* have changed a lot 
  -- since the last time your function was called... Better to just
  -- grab a fresh local reference before doing any major heavy lifting...
  local song = renoise.song()
  
  -- Grab a local ref to the number of tracks in the song, so we can
  -- avoid recounting slow tables and other crap within the loop...
  -- This particular approach *may* seem ugly, but it is actually a *bit*
  -- faster than counting the song track table... 
  -- (The +1 is for the master track)
  local num_tracks = song.sequencer_track_count + song.send_track_count + 1
 
  -- For each track in the song...
  for t = 1, num_tracks do
  
    -- Grab a local ref to the track object we wanna mess with...
    local track = song:track(t)
    
    -- How many devices we got, yo? (Sadly, we have no other way to 
    -- count them at the moment, other than checking the table size...
    local num_devices = #track.devices
    
    -- For each track device in the track...
    for d = 1, num_devices do
    
      -- Is this the device we want?
      if (rawequal(device, track:device(d))) then
      
        -- Thats a bingo!
        return track
        
      end
      
    end
    
  end

  -- Else, womp womp, we found nothing...
  return nil
  
end


-- Run some benchmark test crap...
local test_iterations = 10000
local test_device = renoise.song():track(1):device(1)
local test_functions = { findDeviceInTracksA , findDeviceInTracksB , findDeviceInTracksC }

print('                 ')
for t = 1, #test_functions do
  local f = test_functions[t]
  print('-----------------')
  print(string.format("Running test %i...", t))
  local start_time = os.clock()
  for i = 1, test_iterations do
    f(test_device)
  end
  local stop_time = os.clock() - start_time
  print(string.format("%f (%.2f sec)", stop_time, stop_time))
end
-----------------
Running test 1...
0.416992 (0.42 sec)
-----------------
Running test 2...
0.470215 (0.47 sec)
-----------------
Running test 3...
0.121094 (0.12 sec)
2 Likes

Thanks man! This is very interesting, I wonder if it is really the Lua tables stuff, slowing down here, or the way of accesing the API thru an array/table index. Maybe pairs(track.devices) or #track.devices is as same costly as a track.devices[index] access? If track:device(index) (or track:next_device()) was some kind of iterator or something… Do you know technically, why accessing the Renoise object thru a table index is so slow exactly? Would interest me a lot.

Ok, will update my renoise.song() reference more often, but in every method, that looks kind of ugly to me hehe

1 Like

@ffx

Every time you do…

blah[x].blah[y].blah[z]...

Or even…

blah(x):blah(y):blah(z)...

You are forcing Renoise (lua/luabind) to dig through every link in that chain, find the object in memory, return that object, look up other properties of that object, dig through more properties and objects, etc, etc, etc…

If you do this within a critical loop, then you can easily run into huge CPU overhead.

So, it’s just good practise to reduce that look-up chain as much as possible, and reduce the overhead that Renoise has to do.

If you need a song object, then look it up once locally in your function and re-use it as much as possible.

If you have a loop that is doing a bunch of crap to a track object, then grab a local copy of that track object and work on that local object as much as possible, instead of forcing Renoise to constantly dig through the chain to look up track object properties and other song document stuff over and over again…

Re: tables[] vs function() accessors… I’m certainly no expert on the really low-level C++ luabind crap (@taktik can chime in here…), but it’s my understanding that you’re pretty much forcing Renoise (lua/luabind) to iterate through the whole table to see what’s what, what exists and doesn’t, etc… while the function() accessors are going directly through Renoise to the known object in memory, which is huuuugely faster to deal with. So always use those whenever possible.

Loops are a bitch! :slight_smile:

Make your loops at light as possible, always.

1 Like

I’m surprised that your example is faster. Must mean that a for loop checks the max parameter every iteration? I didn’t know that.

1 Like

It’s more to do with the ipairs() / pairs() stuff… Ultimately, those are just implemented as convenient lua time-savers, so they are definitely nice to use for certain things, but definitely not fully optimised for every situation.

If you really want to squeeze all the possible juice out of the scripting engine, then you should always go as low level as possible (imho). Write your own loops that do exactly what you need, and nothing else.

And do it efficiently :slight_smile:

Ah yeah! I forgot about pairs/ipairs.

But I was wondering about the reason for localizing the max parameter before for loops.

It does feel strange at first, but honestly, the overhead of grabbing that song reference once at the start of a big function that does a bunch of work (on patterns, instruments, tracks, whatever…) is nothing compared to the processing time that all the other loops and stuff will be doing.

And it’s amazing how that simple act of using a local reference can hugely affect your overall performance.

Well worth exploring, imho.

Edit: And even if your main function passes off processing to a bunch of other minor loops or functions, you can still pass the song object through to them as a variable/parameter, to avoid those smaller loops needing to look up the song 1000 times per second or whatever.

1 Like

simple story: renoise.song() is a function that returns an object. Calling functions and returning/creating objects is quite a heavy task. Better do it just once, if possible.

Even the “native” table based lua objects have ‘overhead’ that goes on behind the scenes (setting up and using metatables/metamethods).

1 Like

I believe that Lua re-evaluates the table size on each loop iteration (again, I don’t know with 100% certainty, or exactly how lua/luabind fucks with C++, but…), which obviously introduces a bunch of overhead, so…

As long as you’re confident that the table never changes, then you can optimise this up a level by grabbing the table size before the loop, and then just passing that (hopefully) static max value into the loop, so it avoids the extra (unnecessary) calculations.

That’s been my personal experience in testing at least.

< /nerd >

2 Likes

There also seems to be table.foreach(). I wonder how that one performs…