In this post i'm going to talk about some steps you can take to efficiently use lua in your game engine, to get better performance out of your lua scripts. We'll cover the core differences between C++ and lua, what tasks are better suited for lua, proper communication between lua and C++, and batching C++ calls from lua.
It's important to understand the fundamental differences between lua and C++ so we're going to briefly discuss them here. First off I want to explain the difference between "code" and "scripts" since I will be using them quite a bit in this post. When I say code i'm talking about C++ code, this code gets compiled into machine language, on the other hand when I say "scripts" i'm talking about lua "code" which is interpreted instead of compiled.
Interpreted languages such as lua are never compiled the same way C++ code is, instead it is read by an interpreter which runs the script by calling corresponding machine commands. Something that you may be interested to understand more about how this process works is if you've worked in Visual Studios and you've seen the disassembly of your code which shows your C++ code as if it were in Assembly. Lua has something similar called instruction sets which you can learn more about if you wish but isn't very important to this article.
Lua is (arguably) easier to use, understand, and learn than C++, but more importantly it is quicker to program things in lua. The killer though is that lua is an interpreted language and therefore a single line of lua costs more than a single line of C++ code, there are ways of lowering that cost though. However most of the time this doesn't matter to us, in fact I wouldn't ever worry about it unless you're doing some sort of computational heavy algorithm, parsing lots of data, or anything in massive volume.
As a rule of thumb, if it's gameplay logic do it in lua, if it has to do with the engine then do it in C++. The whole point of using lua is to speed up iteration whether this be gameplay, ui, hud, menus, match making, or anything that requires lots of tweaking. Lua scripts however need to call C++ functions eventually which leads us to the next part.
If you're using lua for gameplay logic then it's more than likely that you're going to be transferring vectors (2,3, or 4 dimensional) back and forth between your C++ engine and lua. You probably also have a math library with lots of functions relating to vectors like dot product, cross product, ect. Now wouldn't it be great to just have lua call your C++ dot product function, C++ is faster right?
Wrong. Well right and wrong, C++ is indeed faster however calling a C++ function costs you, and for each argument you pass to the C++ function it also costs you, same thing with getting a return value (which you can get more than one of in lua.)
With each box in the diagram there is an implied cost for that operation, for every bit of data transferred through that box the cost increases.
Instead of using a C++ dot product function we should use a lua dot product function instead, this will significantly faster than having to transfer the data to C++, do the computation, then transfer the result back to lua. Keeping the cost of this transfer in mind when implementing lua into your engine, as well as using lua in your engine is very important since this can quickly slow your game down.
You may or may not be using reflected C++ classes in your lua implementation (this might not be the right term in describing this but to me it makes the most sense.) If you have a metadata/introspection system in your engine then you can easily tune it to reflect C++ types into lua which will allow you to easily and automatically convert C++ data types to and from lua (by using lua's userdata functionality which allows you to attach binary data onto a lua object.) This allow will help in allowing you to tie C++ functions onto lua objects so that you may take advantage of lua's object oriented features. For instance we could call our C++ dot production function by doing:
local dotResult = myVector:dot(otherVector)
I'll probably write a blog post about the specifics of this later since it is very useful.
Using reflected classes and functions in lua is not always the best idea however, a great example of this would be our vector math library. In this case we should write a vector meta class (a meta class is the lua equivalent of a C++ class) as well as a lua vector math library. Then whenever we transfer a vector from C++ to lua we convert it into a lua vector and if we ever transfer it back to C++ we convert it to a C++ vector.
By doing this we actually increase the cost of transferring a vector back and forth between lua but we in turn cut down on the number of times we have to transfer that vector back and forth. Thinking towards the future and making these kinds of changes in your code and scripts are incredibly important to keeping your game running fast. When asking yourself if you should convert data into a lua meta class instead of sending it as a reflected C++ class, think about what sort of functions are attached to that class, what they do, how expensive they are, how often they are called, and if those functions call other functions which are in C++.
In my Sophomore game project all of our UI and hud were done through lua since it allowed us to quickly prototype and iterate on the design without having to ever close the editor (Script hot reloading is a life saver.) There were some problems that arose from this however that killed our games framerate. Earlier I talked about the cost of transferring data to and from lua and how the more data you transfer the higher that cost gets. What's interesting however is that passing from lua to C++, five 4-byte integers (well actually it would be 8-bytes in lua but nevermind that) costs more then one 20-byte char array. This is big.
Really big. This means that if we package data together into a single argument instead of multiple arguments we can actually improve our performance. The only downside is our code will probably become annoying to read so we should only ever do this when performance is important, or when you have a specific usage for it. Like batching 500 draw calls from lua to C++ into one.
The big problem in some of our hud code was that we were making hundreds of draw calls from lua to C++ per frame. This meant that we had to jump back and forth between lua and C++ and back which becomes incredibly expensive. A solution to this is to instead load all of the data which we would be sending to C++ into an array, then when we have finished making all of our draw calls we send it all to C++ in one function call.
Draw calls are a great example of when you should batch C++ function calls, however don't go batcher crazy. Many things don't require batching, and when asking yourself if you should batch a certain C++ function call you should think about how often that function gets called (once a second, once a frame, a hundred times a frame?), how computationally intense is that function, and if it's even possible to batch that function call (never batch a function like changing an objects position.)
Minimize the number of times you have to go back and forth between lua and C++. Try to map out when and where these transitions are made and find points which have the highest volume and minimize them. By following these steps you will keep your lua scripts running fast and can push the most out of our game engine.