Parallel Join
As mentioned in the chapter dedicated to how to dispatch systems,
Specs automatically parallelizes system execution when there are non-conflicting
system data requirements (Two System
s conflict if their SystemData
needs access
to the same resource where at least one of them needs write access to it).
Basic parallelization
What isn't automatically parallelized by Specs are the joins made within a single system:
fn run(&mut self, (vel, mut pos): Self::SystemData) {
use specs::Join;
// This loop runs sequentially on a single thread.
for (vel, pos) in (&vel, &mut pos).join() {
pos.x += vel.x * 0.05;
pos.y += vel.y * 0.05;
}
}
This means that, if there are hundreds of thousands of entities and only a few systems that actually can be executed in parallel, then the full power of CPU cores cannot be fully utilized.
To fix this potential inefficiency and to parallelize the joining, the join
method call can be exchanged for par_join
:
fn run(&mut self, (vel, mut pos): Self::SystemData) {
use rayon::prelude::*;
use specs::ParJoin;
// Parallel joining behaves similarly to normal joining
// with the difference that iteration can potentially be
// executed in parallel by a thread pool.
(&vel, &mut pos)
.par_join()
.for_each(|(vel, pos)| {
pos.x += vel.x * 0.05;
pos.y += vel.y * 0.05;
});
}
There is always overhead in parallelization, so you should carefully profile to see if there are benefits in the switch. If you have only a few things to iterate over then sequential join is faster.
The par_join
method produces a type implementing rayon's ParallelIterator
trait which provides lots of helper methods to manipulate the iteration,
the same way the normal Iterator
trait does.