Best practice for passing/processing events


#1

Heho!
I am looking for tips to achieve proper performance for an event-passing-system using chaiscript. So i have a game-engine that that generates events in c++ and script (little structs) and i pass these to a (bound) chaiscript-function which puts them in a chaiscript Map() to be later on handled by a bound call to script which executes another script-func that feeds those events to scripted handlers. This seems to be about the worst way to do this, and is too slow to be feasible. It was just what i threw together while prototyping, and now i want to do this part the right way.
I have a class instance in chaiscript which holds maps (events,handlers/listeners), and maps-o-functions to do stuff with these - which i think is the first mistake i made. As i understand by now globals are a bad thing in chaiscript, and this class was thought out to be accessed as a global singleton. And it would be used a lot.
Am i right in the assumption that not really passing events to script to be stored there, but calling bound handlers from c++ side feeding them events from a c++ storage would be proper? Meaning i would register script-functions using a c++ function from withing chaiscript, as i want handlers to be scripted, but would move calling these outside the script.
I can imagine a lot of different combinations of registering/calling/passing between c++/chai, and would appreciate input/ideas on the most performant way.
Right now just passing down the events, and calling the handlers eats up all the processing time, even with empty handlers. I am not sure what part exactly it is that is so slow, if it is the fact that every queue/handle needs to get a global obj, or the maps, or having stupid copying of events somewhere in there (probably).


#2

I’ll try to give you a run down on performance of various calls inside of ChaiScript:

In order of fastest to slowest:

Performance of Various Calling Methods

Local variable lookup should be fastest

var f = some_function;
f(obj);

This is only the fastest option if you are calling some_function many times

Dot access is second fastest, because it only looks in the function namespace

obj.some_function()

Global variable lookup is next fastest

some_global_variable_containing_a_func(obj);

Finally regular function calls

some_function(obj);

Avoid conversions

Any conversion that you require to make your function call adds runtime overhead. Calling with the most exact parameters possible is the best option.

Object Lookup Process

Functions are treated as objects and function calls must first determine the best option to call, so referencing the info above, when calling a function (or looking up any value):

  1. First locals are searched
  2. Then globals
  3. Then functions

I have the eventual goal of eliminating the local lookup when possible (or making it even faster, when it is necessary) because I should be able to know at parse time what locals are available.

Both global lookups and function lookups require a mutex lock. This is why disabling threading support (with -DCHAISCRIPT_NO_THREADS) gets you the best performance possible.

I’ve also played with maintaining local thread caches of the current set of functions and globals to avoid those mutex locks, but the overhead of maintaining the cache overwhelms the gains from avoiding the mutex.

Best Practices

IMO, from a code cleanliness standpoint and ease of use, I prefer registering strongly typed functions. It would also move more of your code from ChaiScript to C++. In general, prefer to implement anything performance critical in C++. This, to me, is the biggest strength of ChaiScript - it’s so easy register C++ functions that you should put as much code as possible in C++. OR, prototype it in ChaiScript them move it to C++ once you know where your bottlenecks are.

So, this is what I would recommend for callbacks and events, (hopefully I’m answering your question here):

//C++

void add_callback(const std::string &t_name, const std::function<bool (const EventObject&)> &t_func)
{
  m_callbacks[t_name] = t_func;
}

int main()
{
  ChaiScript chai;
  chai.add(fun(&add_callback), "add_callback");
  chai.add(user_type<EventObject>(), "Event_Object"); 
  // add the rest of your EventObject methods/members here, etc
}

// chaiscript

add_callback("Event Name", fun(EventObject o) { /* do something with o */ });

Something like that, I’d think. It gives you decent performance, flexibility, and type safety.


#3

Thank you lefticus. That totally answered my question(s).
I changed my code to something similar to your example, and cpuload dropped to reasonable heights. Added the add_callback equivalent and brethren using add_global_const though, as some of them might get called often, and i want them to be immutable.
(now if functions added per chai::add(chai::fun(),“f”) are locals i haven’t understood anything).

Taking an extra look for possible conversions and copies led me to discover that my Events did not move properly, but were copied in all the places i wanted to move them (botched move-constructor).

Now the physics can spew out tons of Events per frame without the whole thing going up in smoke, and i can go to sleep with my mind at ease for now.


#4

I believe you understood correctly.

I’d be curious if you can measure how much of a performance difference this is for you between using the functions as global objects or added via as functions directly.


#5

A quick measure over 1000000 handlers added either way resulted in non-global being ~9.5% slower compared to global.
I don’t have CHAISCRIPT_NO_THREADS set.


It is posisble to inherit in chaiscript?
#6

Ok, that’s a pretty good mark to hit. With some work maybe I can get functions lookup on par with global lookup. Share the same mutex lock, perhaps? Maybe some look-ahead to determine where a function is coming from, something like that.