Not a member yet? Why not Sign up today
Create an account  

  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
 
Lua optimization observations

#1
Since optimization in FtD Lua can be very different from optimization in other languages and environments such as C or Java, I thought it might be useful to collect information about FtD Lua specific optimization. (but I also include some more general optimization-related stuff)

The main goal would be to find the fastest alternative when there are multiple ways to do something. The results below are based on benchmarks executed in FtD. The specific quantitative differences may depend on the machine used, but the qualitative differences should be mostly universal. Especially the items listed under 'Major FtD specific' are worth keeping in mind as they are not obvious but can have a major impact on performance.

The list below is by no means complete; I just wrote down the first things that came to mind. Any suggestions for additions, changes or clarifications are always welcome.

Major FtD specific:
the math library seems to be much faster than the Mathf library. This means that whenever possible, use function in the math library such as math.sqrt instead of the equivalent functions in the Mathf library such as Mathf.Sqrt.

When working with a Vector3 instance v, accessing its members with v[1], v[2], v[3] is orders of magnitude faster than accessing them with v.x, v.y, v.z. The same is not true for normal Lua tables.

Creating a Vector3 instance takes roughly twice as long as creating an array (table) with three entries.

For a Vector3 instance v, avoid using v.sqrMagnitude and v.magnitude as they are much slower than the respectively equivalent Vector3.Dot(v,v) and math.sqrt(Vector3.Dot(v,v)).

The (deprecated) I:GetMissileInfo is incredibly slow compared to I:GetLuaControlledMissileInfo, so only use the former if you can't do the same with the latter.

Major general:
Always use the local keyword when defining functions and variables, even if they are used as a global function or variable. The only exception is the Update function which must be global. Performance comparison of 'local global' variables and truly global variables:
Code:
function Update(I)
  for i = 1, 10000000 do
    
  end
end
runs in 26ms as the baseline for the next two benchmarks,
Code:
b = nil --truly global variable. Don't ever do this.
function Update(I)
  for i = 1, 10000000 do
    b = 0
  end
end
runs in 54ms while
Code:
local b = nil --note the local keyword here
function Update(I)
  for i = 1, 10000000 do
    b = 0
  end
end
runs in only 28ms. Adding the local keyword can not change the functionality as everything we do is within its scope, but it improves performance from 28ms over the baseline to only 2ms over the baseline. Interestingly, it only seems to matter when writing to the variable, not when reading it.

Avoid goniometric and inverse goniometric functions whenever possible. From a benchmark, it seems that math.sin and math.cos take at least 4 times longer than math.sqrt and math.tan, math.acos, math.asin and math.atan take at least 5 times longer. In many cases it is possible to replace some goniometric functions with square roots such as
Code:
local cosa = math.cos(a);
local sina = math.sqrt(1 - cosa * cosa);
instead of
Code:
local cosa = math.cos(a);
local sina = math.sin(a);

Minor FtD specific:
A call to math.abs takes slightly longer than call to math.sqrt (which is weird). Replacing math.abs with an inline if-else is faster, creating your own abs function to use is slower.

Minor general:
When using global functions more than once within the same local scope, making a local reference can have a significant impact on performance. That is,
Code:
function bar()
  local foo = foo;
  foo();
  foo();
end

function foo()
end
will have faster execution of bar than
Code:
function bar()
  foo();
  foo();
end

function foo()
end
This extends to build in libraries such as math and Vector3. Making a local reference to the library or to specific functions within the library will improve performance if they are used more than once.

You can go even further with this: when using a self-made function that, for example, uses some global function, it is executed faster if the function is created inside a closure that contains a local reference to the global function that's used. An example for the case of calculating the magnitude of a vector:
Code:
function createMagnitudeFunction() --this function is just here to create a closure
  local sqrt = math.sqrt; --create a local reference to the square root function
  function magnitudeFunction(x)
    local x1 = x[1];
    local x2 = x[2];
    local x3 = x[3];
    return sqrt(x1 * x1 + x2 * x2 + x3 * x3); --we use the external local reference sqrt instead of using math.sqrt
  end
  return magnitudeFunction; --we return the function
end
magnitudeFunction = createMagnitudeFunction(); --create the actual function and assign it to a variable;
The performance gain by doing this is small but measurable. Readability does suffer badly though. The function in this example is faster than both v.magnitude and math.sqrt(Vector3.Dot(v,v)).
Reply

#2
Using locals for everything is an old generic lua idea - that includes variables in the main file scope ( ie outside any function ), or they'll go into the global table - or at least that's what happened a few versions ago. I tend also to create a local for libs I'm using a lot.

Closure based OOP is also considerably faster at the expense of some memory.
Poke my boat! mostly pre-2.0 learning & catalogue thread - Update: Heavy & light tanks 07/04/18 for 2.1. 6 ships made 2.0 aware. No more post-processing! finally! but now I can't read the forum.
Reply

#3
(2018-03-22, 09:42 PM)Richard Dastardly Wrote: Using locals for everything is an old generic lua idea - that includes variables in the main file scope ( ie outside any function ), or they'll go into the global table - or at least that's what happened a few versions ago. I tend also to create a local for libs I'm using a lot.

Closure based OOP is also considerably faster at the expense of some memory.

Yeah, using locals is pretty general, but I still decided to add it because it is specific to scripting languages mostly.

What exactly do you mean by closure based OOP?
Reply

#4
(2018-03-22, 09:55 PM)HerpeDerp Wrote: What exactly do you mean by closure based OOP?

Code:
local Class = {}

Class.Example = function( v )
      local self = {}
      local val = v
      self.public_var = 0

      function self.Inc()
          val = val + 1
      end

       function self.Val( v )
           if v then val = v end
           return val
       end

       return self
end

Class.Example2 = function(v)
       local self = Class.Example(v)

       function self.Raise( p )
           return self.Val()^p
       end

       return self
end

Now I realise just how long since I've done any lua beyond fiddling so that might not be completely correct, but the idea should be obvious. Lua-users has a big article somewhere.
Poke my boat! mostly pre-2.0 learning & catalogue thread - Update: Heavy & light tanks 07/04/18 for 2.1. 6 ships made 2.0 aware. No more post-processing! finally! but now I can't read the forum.
Reply

#5
(2018-03-23, 03:16 AM)Richard Dastardly Wrote: -snip-

Ah, yes, of course. The performance benefit has the same origin as in the last example in my post: you want to keep everything 'as local as possible'. You do have to be careful though that you don't add so much overhead that you lose the advantage you gained, which can happen quite quickly when trying to write Lua object oriented. Continuing from your example, when using it as
Code:
local a = Class.Example();
a.Val();
then in the last call, you still search through four hash tables (more actually, but the four tables I list are unavoidable regardless of how you want to use it): Once to find a, once to find Val within a, once to search for val within the closure of Val (but it's not there) and once to find val within the closure of Class.Example. This is performance-wise only an improvement over more straightforward methods if it happens nested deep enough.

I'm personally not too big on using Lua OO. I do sometimes use closures when useful but only if it doesn't obfuscate too much
Reply

#6
(2018-03-23, 04:08 AM)HerpeDerp Wrote: I'm personally not too big on using Lua OO. I do sometimes use closures when useful but only if it doesn't obfuscate too much

That was contrived - I'd probably just make val a member of the base class's self table & so completely public, it's not like you tend to write lua if you want strict encapsulation... but yes, point still stands about the amount of lookups going on.

In times past I've written some pretty sizeable lua projects & there's a fairly obvious point where you just have to be a bit more formal given how fluid & small the language is, but agreed unless you're doing it for clarity I don't think FtD-sized projects are generally going to bother. You can make closures on the fly in do-end blocks anyway, if you must.

Did you do any timing of calls to FtD API funcs, btw? number 1 saving for a lot of FtD lua I've seen would be to cache the results of the first call in a tick & don't do another API call... ( encapsulating API calls is one nice use of closures, especially as you can then tell it to only return the actual API call value every X ticks - but that's getting into OOP territory again ).

Code:
local function a()
  return 1
end

function b()
  return 1
end
Any noticeable difference in cost between those? function b() is the same as saying b = function(), so declaring them all local in the scope of the file should do something.
Poke my boat! mostly pre-2.0 learning & catalogue thread - Update: Heavy & light tanks 07/04/18 for 2.1. 6 ships made 2.0 aware. No more post-processing! finally! but now I can't read the forum.
Reply

#7
(2018-03-24, 12:34 AM)Richard Dastardly Wrote: -snip-

I may need to specify my previous statement a bit more: in my opinion, OOP style Lua improves clarity for large projects due to the added structure but reduces clarity for small projects due to the (relatively) large amount of extra code needed. This is probably more personal preference than anything objective.

I have not timed FtD API calls yet, but I was already planning on doing those next. I have always cached their returns in my scripts though. I have a habit of caching nearly everything anyway.

Btw, have you been able to explain and/or reproduce the result that math.sqrt takes a tiny bit longer than math.abs? It seems really odd to me.
Reply

#8
Does anyone have a good explanation why
Code:
local a = nil;
local b = 0;
function Update(I)
  for i = 1, 10000000 do
    a = b;
  end
end
is significantly faster than
Code:
a = nil; --this is the difference
local b = 0;
function Update(I)
  for i = 1, 10000000 do
    a = b;
  end
end
(it is obvious why this would be slower of course), but
Code:
local a = nil;
local b = 0;
function Update(I)
  for i = 1, 10000000 do
    a = b;
  end
end
is not any faster than
Code:
local a = nil;
b = 0; --this is the difference
function Update(I)
  for i = 1, 10000000 do
    a = b;
  end
end

Why is there a significant difference between the truly global scope and the most global local scope when writing but not when reading?
Reply

#9
Some quirk of closures? I think the for loop gets turned into a closure. It's always going to write to global a, but maybe it's cached b somehow. Try a = b; b = a ?
Poke my boat! mostly pre-2.0 learning & catalogue thread - Update: Heavy & light tanks 07/04/18 for 2.1. 6 ships made 2.0 aware. No more post-processing! finally! but now I can't read the forum.
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)