Skip to content
This repository was archived by the owner on Jun 1, 2022. It is now read-only.
This repository was archived by the owner on Jun 1, 2022. It is now read-only.

Engine: Slow retrieval of components #67

Description

@joeltio

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:
getComponent is the largest in all operations
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:
asCompSet, Composable constructor, filterEntityId lambda are the main costly functions
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

  1. The result of ics::asCompSet can be cached or even stored and maintained by CompStore, effectively removing the cost of constructing an unordered_set.
  2. 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.
  3. 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.
  4. 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions