Since I'm learning about game engine development, I'll take notes and document the things I learn and my development process.
At the time of writing this post, I have a very basic 3D rendering engine with a custom ECS (Entity Component System) architecture. I can draw simple geometries with custom colors, move the camera around, as well as rotate it with the mouse, as shown in this GIF:
Doing premature optimization
I spent last weekend working on improving my ECS, and I'll show my process for doing it.
Benchmark
The first step was to benchmark the library's spawn and query performance, and the results were neither decent nor satisfactory:
Spawning 1 million entities (very basic entities with 1-2 components each) took 5 seconds.
Querying those entities took almost 3 seconds.
The code for spawning looks something like this:
#[derive(Component)]
struct Position(u128, u128);
#[derive(Component)]
struct Velocity(u128);
fn main() {
let mut world = World::new();
for i in 0..=1_000_000 {
world.spawn((Position(i, i), Velocity(i)));
world.spawn(Velocity(i));
}
let mut query = world.query::<(&Position, &Velocity)>();
query.iter().for_each(|(position, velocity)| {
assert_eq!(position.0, position.1);
assert_eq!(position.0, velocity.0);
})
}
Profiling
And well, after seeing those horrendous benchmark results, I used flamegraph to determine what was consuming the most CPU time.
Spawning
The first thing I noticed was that the Entity creation had the largest share of the result, as you can see below:
The problem was that I was passing a vector of tuples to the Entity
constructor and using the DashMap::from_iter_par
method, which creates the map from a parallel iterator. However, for some reason, it was taking a lot of time to do it.
The solution I've come to is that instead of creating the map from the vector and storing it, I stored the vector itself. It became significantly faster and didn't affect query performance.
Querying
The query performance wasn't too bad; indeed, it was okay since I was doing it single-threaded. Anyway, I did some refactoring to be able to use the parallel iterator from rayon, and it improved a bit, though not drastically. Then, I noticed that DashMap
was using Rust's default hasher, which isn't quite as speedy as I wanted. So, I changed it to use FxHasher, which is much faster than the default one. With this change, the query operation now takes 0.4 seconds, as shown in the following image:
You can ignore the Positions and Velocity logs since I was using them just for debugging purposes.
Well, this was my process of refactoring and optimizing some parts of my ECS library. I hope you liked it. See you in my next post.
GitHub repository: woody
Top comments (0)