Investigate the cost of performing an ordered insert of Vehicle
s into the lane. If the Vehicle
s are ordered according to position on the road, detecting the distance of two Vehicle
s is child's play:
Eg
for (size_t n = 0; n < fVehicles.size() - 1; n++)
{
distance = fVehicles[n].getPosition() - fVehicles[n+1].getPosition();
}
This is O(N) vs O(N^2) (using ^ as exponent, not XOR). The price of this simplification is the requiring ordered insert into fVehicles
, and that should be O(N): One std::binary_search
to detect the insertion point and whatever shuffling is required by fVehicles
to free up space to place the Vehicle
.
Maintaining ordering of fVehicles
may be beneficial in other places as well. Visualizing the list (graphically or by print statements) will be much easier, debugging is generally easier on the human brain when everything is in a nice predictable order, and CPUs... They LOVE going in a nice, predictable straight line. Sometimes you get a performance boost that you didn't see coming. Great write-up on that here: Why is it faster to process a sorted array than an unsorted array?
Only way to be sure if this is better is to try it and measure it.
Other Suggestions:
Don't use pointers to the vehicles.
Not only are they harder to manage, they can slow you down quite a bit. As mentioned above, modern CPUs are really good at going in straight lines, and pointers can throw a kink in that straight line.
You never really know where in dynamic memory a pointer is going to be relative to the last pointer you looked at. But with a contiguous block of Vehicle
s , when the CPU loads Vehicle
N it can possibly also grab Vehicle
s N+1 and N+2. If it can't because they are too big, it doesn't matter much because it already knows where they are, and while the CPU is processing, and idle memory channel could be reading ahead and grabbing the data you're going to need soon.
With the pointer you save a bit every time you move a Vehicle
from lane to lane (pointers are usually much cheaper than objects to copy), but may suffer on each and every loop iteration in each and every simulation tick and the volume really adds up. Bjarne Stroustrup, God-Emperor of C++, has an excellent write up on this problem using linked lists as an example (Note linked list is often worse than vector
of pointer, but the idea is the same).
Take advantage of std::deque
.
std::vector
Is really good at stack-like behaviour. You can add to and remove from the end lightning fast, but if you add to or remove from the beginning, everything in the vector
is moved.
Most of the lane insertions are likely to be at one end and the removals at the other simply because older Vehicle
s will gravitate toward the end as Vehicle
s are added to the beginning or vise versa. This is a certainty if suggestion 1 is taken and fVehicles
is ordered. New vehicles will be added to the lane at the beginning, a few will change lanes into or out of the middle, and old vehicles will be removed from the end. deque
is optimized for inserting and removing at both ends so adding new cars is cheap, removing old cars is cheap and you only pay full price for cars that change lanes.
Documentation on std::deque
Addendum
Take advantage of range-based for
where possible. Range-based for takes most of the iteration logic away and hides it from you.
Eg this
for (unsigned int iV = 0; iV < fVehicles.size(); iV++)
{
for(unsigned int jV = 0; jV < fVehicles.size(); jV++)
{
forwardDistance = fVehicles[iV]->getPosition() - fVehicles[jV]->getPosition();
}
}
becomes
for (auto v_outer: fVehicles)
{
for (auto v_inner: fVehicles)
{
forwardDistance = v_outer->getPosition() - v_inner->getPosition();
}
}
It doesn't look much better if you are counting lines, but you can't accidentally
iV <= fVehicles.size()
or
fVehicles[iV]->getPosition() - fVehicles[iV]->getPosition()
It removes the possibility for you to make silly, fatal, and hard-to-spot errors.
Let's break one down:
for (auto v_outer: fVehicles)
^ ^ ^
type | |
variable name |
Container to iterate
Documentation on Range-based for
In this case I'm also taking advantage of auto
. auto
allows the compiler to select the type of the data. The compiler knows that fVehicles
contains pointers to Vehicle
s, so it replaces auto
with Vehicle *
for you. This takes away some of the headaches if you find yourself refactoring the code later.
Documentation on auto
Unfortunately in this cans it can also trap you. If you follow the suggestions above, fVehicles
becomes
std::dequeue<Vehicle> fVehicles;
which means auto
is now Vehicle
. Which makes v_outer
a copy, costing you copying time and meaning if you change v_outer
, you change a copy and the original goes unchanged. to avoid that, tend toward
for (auto &v_outer: fVehicles)
The compiler is good at deciding how best to handle that reference or if it even needs it.