Summary
After profiling the engine, it seems that the main bottleneck is in ics::getComponent, due to the use of Composable and its subsidiary functions such as ics::asCompSet.
Observations
I profiled the following code within a test in engineService.test.cpp:
for (int i = 0; i < 80000; i++) {
StepSimulationReq stepReq;
StepSimulationResp stepResp;
grpc::ServerContext stepContext;
stepReq.set_name(testSim.SIM_NAME);
engineSvc->StepSimulation(&stepContext, &stepReq, &stepResp);
}
Here is the flame graph:

Notice how ics::getComponent forms the largest chunk in each operation. Zooming in on the function, we can see its subsidiary functions which are slow:

Note that the third function from the left is a lambda produced by filterEntityId from the EntityIndex.
Just as a reminder, the file calling these methods are in sim/src/ics.cpp:
auto components = util::Composable<ComponentStore&>(compStore) |
ics::asCompSet | compTypeIndex.filterCompType(typeName) |
entityIndex.filterEntityId(entityId);
Recommended Fix
- The result of
ics::asCompSet can be cached or even stored and maintained by CompStore, effectively removing the cost of constructing an unordered_set.
- The
util::Composable construction and usage is known at compile-time, therefore, it should not incur any cost. It should be implemented as a constexpr or something similar.
- The main cost incurred in the
filterEntityId lambda is actually the set intersection operation. ics::asCompSet should be changed to return an ordered set (this will not incur much cost since the entity IDs and group IDs are already ordered), and the C++ STL set_intersection should be used. See more details below on the reason.
compTypeIndex.filterCompType can be converted to return an iterator to reduce costs incurred in creating a new set.
In conclusion, make things doable at compile-time done at compile-time and convert the functions to generators.
More details on recommended fix 3.
The main cost incurred in the filterEntityId lambda is actually the set intersection operation. The set intersection (written by me) creates a new set and inserts intersecting elements into that set. The high cost of insert is the problem:
template <class T>
std::unordered_set<T> setIntersection(const std::unordered_set<T>& a,
const std::unordered_set<T>& b) {
std::unordered_set<T> newSet;
for (const T& aElement : a) {
if (b.contains(aElement)) {
newSet.insert(aElement);
}
}
return newSet;
}
Hence, we want to remove the construction of newSet and the insert operation.
To do so, we need to convert this function into a generator, i.e. a function that returns an iterator. Due to the nature of this operation, the iterator returned must be a forward iterator (i.e. an iterator that can only be incremented). Assuming that the inputs are also forward iterators (to ensure composability), this is not possible with unordered sets, but possible with ordered sets.
The C++ STL implementation of set intersection does exactly this. It takes in two iterators of ordered sets and returns an iterator of the set intersection.
Therefore, for compatibility, the filterEntityId code should call the STL set intersection and use ordered_sets to store compStoreIds
Summary
After profiling the engine, it seems that the main bottleneck is in
ics::getComponent, due to the use ofComposableand its subsidiary functions such asics::asCompSet.Observations
I profiled the following code within a test in
engineService.test.cpp:Here is the flame graph:


Notice how
ics::getComponentforms the largest chunk in each operation. Zooming in on the function, we can see its subsidiary functions which are slow:Note that the third function from the left is a lambda produced by
filterEntityIdfrom theEntityIndex.Just as a reminder, the file calling these methods are in
sim/src/ics.cpp:auto components = util::Composable<ComponentStore&>(compStore) | ics::asCompSet | compTypeIndex.filterCompType(typeName) | entityIndex.filterEntityId(entityId);Recommended Fix
ics::asCompSetcan be cached or even stored and maintained byCompStore, effectively removing the cost of constructing anunordered_set.util::Composableconstruction and usage is known at compile-time, therefore, it should not incur any cost. It should be implemented as aconstexpror something similar.filterEntityIdlambda is actually the set intersection operation.ics::asCompSetshould be changed to return an ordered set (this will not incur much cost since the entity IDs and group IDs are already ordered), and the C++ STLset_intersectionshould be used. See more details below on the reason.compTypeIndex.filterCompTypecan be converted to return an iterator to reduce costs incurred in creating a new set.In conclusion, make things doable at compile-time done at compile-time and convert the functions to generators.
More details on recommended fix 3.
The main cost incurred in thefilterEntityIdlambda is actually the set intersection operation. The set intersection (written by me) creates a new set and inserts intersecting elements into that set. The high cost ofinsertis the problem:Hence, we want to remove the construction of
newSetand the insert operation.To do so, we need to convert this function into a generator, i.e. a function that returns an iterator. Due to the nature of this operation, the iterator returned must be a forward iterator (i.e. an iterator that can only be incremented). Assuming that the inputs are also forward iterators (to ensure composability), this is not possible with unordered sets, but possible with ordered sets.
The C++ STL implementation of set intersection does exactly this. It takes in two iterators of ordered sets and returns an iterator of the set intersection.
Therefore, for compatibility, the
filterEntityIdcode should call the STL set intersection and useordered_sets to storecompStoreIds