Understanding a bottleneck with sol:objects
Hello,
I'm currently using Sol to create a view into my ECS, doing this creates a callback from C++ and uses sol:make_object every time a component is requested from an entity. Right is this sol:make_object is my largest bottleneck just simply due to the potential for there to be thousands of these calls per frame. Using a sol:function to create the Lua function callback, and then another function to retrieve the component from the entity (which is my bottleneck due to constructing the sol:object every request)
I'm mostly just wondering if this is the normal workflow, if constructing these sol:objects every time I want to grab the component from C++ into Lua, or if I'm supposed to be creating the object once and keeping it on the component. Even if there could be thousands of these components active at a time.
I'd love some feedback on the proper way to handle this!
Thanks again!
Every time you call sol::make_object or other make_ functions, a new reference is created and put into its own slot in Lua's registry, which is a big table of all the Lua values that the host program needs to hold onto. That stuff has an allocation cost and burdens the garbage collector.
I'm not an expert but I know of two ways to avoid this.
Option 1, the better option, I describe here: https://github.com/ThePhD/sol2/issues/1737#issuecomment-3565333444 Essentially, you have your object manage its own reference by holding onto a sol::userdata/object/reference that only gets created once. One nice thing about this is it makes it easy to always capture the most derived type if you have a class heirarchy. If the C++ object can outlive its reference you may want to look after that, or be at peace with the resulting undefined behavior. On the other hand if the Lua reference can keep the C++ object alive, you may need to use a weak table to track the references that exist for your C++ objects, so that they don't keep themselves alive forever.
Option 2 is much more low-level. lightuserdata is Lua's way of handling a raw pointer (akin to void*) which isn't reference counted and by default doesn't come with any functionality. You can make lightuserdata into a rich type with debug.setmetatable and custom type-checking. Anything you do here will be highly application-specific and you'll probably need to come up with some way to check types and lifetimes for every lightuserdata passed back from Lua. I have not attempted this so I can't really speak to all the pitfalls.
Every time you call
sol::make_objector othermake_functions, a new reference is created and put into its own slot in Lua's registry, which is a big table of all the Lua values that the host program needs to hold onto. That stuff has an allocation cost and burdens the garbage collector.I'm not an expert but I know of two ways to avoid this.
Option 1, the better option, I describe here: https://github.com/ThePhD/sol2/issues/1737#issuecomment-3565333444 Essentially, you have your object manage its own reference by holding onto a
sol::userdata/object/referencethat only gets created once. One nice thing about this is it makes it easy to always capture the most derived type if you have a class heirarchy. If the C++ object can outlive its reference you may want to look after that, or be at peace with the resulting undefined behavior. On the other hand if the Lua reference can keep the C++ object alive, you may need to use a weak table to track the references that exist for your C++ objects, so that they don't keep themselves alive forever.Option 2 is much more low-level.
lightuserdatais Lua's way of handling a raw pointer (akin tovoid*) which isn't reference counted and by default doesn't come with any functionality. You can makelightuserdatainto a rich type withdebug.setmetatableand custom type-checking. Anything you do here will be highly application-specific and you'll probably need to come up with some way to check types and lifetimes for everylightuserdatapassed back from Lua. I have not attempted this so I can't really speak to all the pitfalls.
I appreciate the detailed reply! I’m still a bit confused about one thing. The understanding is that sol::objects themselves push into the Lua registry, which adds allocation overhead and puts some burden on the garbage collector.
If you store a sol::object (as you suggested), do these costs not apply? Is that the recommended workflow? For example, when retrieving a component from my ECS, I create a new sol::object from its type, purely on demand, they are discarded after use. Could I instead maintain a registry of components that have already been accessed in Lua and keep their sol::objects alive? Would that still cause garbage collector overhead?
I don’t have to worry too much about lifetimes because my Lua scripts function as stateless systems, they essentially poll components on demand, similar to a standard C++ ECS workflow. But this also means that every single component (and there could be tens of thousands or more) get pushed onto the Lua stack every time a system runs.
Thanks again for the clarification!