#Optimalization through multithreading
316 messages ยท Page 1 of 1 (latest)
When your question is answered use !solved to mark the question as resolved.
Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question run !howto ask.
according to the profiler I should first look at update_rockets
void Tmpl8::Game::update_rockets()
{
for (Rocket& rocket : rockets)
{
rocket.tick();
//Check if rocket collides with enemy tank, spawn explosion, and if tank is destroyed spawn a smoke plume
for (Tank& tank : tanks)
{
if (tank.active && (tank.allignment != rocket.allignment) && rocket.intersects(tank.position, tank.collision_radius))
{
explosions.push_back(Explosion(&explosion, tank.position));
if (tank.hit(rocket_hit_value))
{
smokes.push_back(Smoke(smoke, tank.position - vec2(7, 24)));
}
rocket.active = false;
break;
}
}
}
}
So what I think I should do is look at the available threads on the machine, and then divide the rockets vector over them
send the pastebin link here too
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
This is the entire main file
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
Line 169:
std::vector<Tank*> nearby_tanks = tank.owner_cell->tanks;
and this is the threadpool
this doesn't have to be a copy
it could be a const ref to the tanks field
auto const& nearby_tanks = tank.owner_cell->tanks;
I see
there is only one .reserve() call on vector in your code
and 11 .push_backs
reserve memory ahead so it won't be reallocated many times
estimate how many items will be stored
Okay
IDK what does add_tank do, but this is most likely unsafe
line 90
you're taking a reference to a vector element
in a loop
in which you're pushing back to it
/// <summary>
/// Voegt tank toe aan een cell gebaseerd op zijn positie
/// </summary>
/// <param name="tank"></param>
void Grid::add_tank(Tank* tank) {
Cell* cell = get_cell(tank->position);
add_tank(tank, cell);
}
/// <summary>
/// Voegt tank toe aan cell
/// </summary>
/// <param name="tank"></param>
void Grid::add_tank(Tank* tank, Cell* cell) {
cell->tanks.push_back(tank);
tank->owner_cell = cell;
// Geef de positie van de tank in de vector array aan:
// Omdat hij aan de achterkant van de vector staat (push_back) is de positie
// de grootte van de vector min รฉรฉn.
tank->cell_vector_index = cell->tanks.size() - 1;
}
Sorry it is in dutch
yeah, it is unsafe
std::vector<Cell*> neighbouring_cells = grid->get_neighbour_cells(¤t_tank);
std::vector<Tank*> neighbouring_enemy_tanks = grid->get_neighbouring_tanks(neighbouring_cells, current_tank.allignment);
line 113-114
use references here
same as before
Cool
unless you have very good reason not to use it
I have added one .reserve for the particle_beams vector as that will always be 3
modern IDEs can show you inferred types anyway
also, not a performance problem, but I will mention it anyway
try not to repeat yourself with types
no need to repeat it multiple times :p
The performance profiler, it only shows in depth one loop
at least I think
let me try that again
Ask yourself if all of the ticks have to be performed at the same rate?
From my experience most of game ticks can be limited to 10 times per second
I know the tanks_update one will be used a lot
I'm not talking about core gameplay mechanics
ah like that
and physics handling
this should tick each frame
but cosmetics
could be limited not to tick every frame
also prefer early return
I'm a Never Nester and you should too.
Access to code examples, discord, song names and more at https://www.patreon.com/codeaesthetic
Correction: At 2:20 the inversion should be "less than or equal", not "less than"
same with loops and their continue;
nesting adds complexity and is unnecessary
this guy explained it well
Yea I definitely agree with this
but I didn't write most of this code
If I change it somewhere I'll have to change it everywhere
and I don't really wanna spend loads of time refactoring the code to make it more readable
okay I understand
I know personally though I wouldn't write it this way
Does the profiler make your program run slower?
alright good I thought I might've f'd something up ๐
what compiler do you use?
Is this the right way to go, if I wanted to multithread this?
MSVC?
GCC?
Clang?
If you're running on MSVC you can easily adapt multithreading in certain places
I honestly am not sure, visual studio handles all of that
we aren't allowed to use any of those libraries unfortunately
would make it a lot easier
this is the standard library
C++17 to be exact
only the thread library and the threadpool they provided
in C++17 they implemented parallel versions of algorithms
yeah, but I doubt std lib is not allowed
Oh yeah sure that is allowed
you can for example run for_each in parallel
hmm
#include <execution>
#include <algorithm>
// ...
std::for_each(std::execution::par, vec.begin(), vec.end(),
[&](VecElement const& elem) {
// do something
});
no need to manually launch threads
most of your loops that do not write to a common memory
could be rewritten this way
and possibly using std::scoped_locks on common data
example
void Game::update_particle_beams() {
for (Particle_beam& particle_beam : particle_beams)
{
particle_beam.tick(tanks);
//Damage all tanks within the damage window of the beam (the window is an axis-aligned bounding box)
for (Tank& tank : tanks)
{
if (tank.active && particle_beam.rectangle.intersects_circle(tank.get_position(), tank.get_collision_radius()))
{
if (tank.hit(particle_beam.damage))
{
smokes.push_back(Smoke(smoke, tank.position - vec2(0, 48)));
}
}
}
}
}
could be
void Game::update_particle_beams() {
for (Particle_beam& particle_beam : particle_beams)
{
particle_beam.tick(tanks);
//Damage all tanks within the damage window of the beam (the window is an axis-aligned bounding box)
// ...
std::for_each(std::execution::par, tanks.begin(), tanks.end(),
[&](Tank& tank) {
if (!tank.active || !particle_beam.rectangle.intersects_circle(tank.get_position(), tank.get_collision_radius()))
return;
if (!tank.hit(particle_beam.damage))
return;
auto lock = std::scoped_lock(smokes_mtx);
smokes.push_back(Smoke(smoke, tank.position - vec2(0, 48)));
});
}
}
Aha
make sure to properly protect smokes vector with the smoke_mtx
nifty method
this will give you a huge performance boost if applied properly
unfortunately the profiler data isn't loading
after finishing
I will try this
but I am 90% sure I am supposed to use the threadspool they provided
even though this is clearly a better option
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
you can even write your own impl of the for_each using the threadpool
pretty easily
template <typename Range, typename Func>
void par_for_each(ThreadPool& tp, Range& range, Func func) {
// assuming you have an 8 core processor
auto len = range.size();
auto num_threads = std::min(16, range.size());
auto step = len / num_threads;
for (size_t i = 0; i < len; i += step)
tp.enqueue([i, step, &func]{
auto curr = std::next(std::begin(range), i);
auto end = std::next(curr, step);
for (; curr != end; ++curr)
func(*curr);
});
}
}
hm, you should also hold futures somewhere
welp
im struggling trying to lock the smokes even
so i make a std::mutex and I call .lock on that
this is how launching N threads over a number of elements work
do not call .lock() manually
use:
{
auto lock = std::scoped_lock(mutex);
// do thing here
// automatically unlocked
}
so you dont have to do anything with the lock?
no, scoped_lock does what its name suggests
yes
even if you write return; in between
so it is much safer than manually calling .lock()
and I should create the mutex then before the for_each
you should create a mutex wherever you create the thing it protects
but
if you're going for the safer and more managable approach
the smokes vector is made in the header file
which is to have the main thread
and then launch simple worker threads for smaller tasks
that don't exit the function that creates them
you can have a local mutex
in that function
you'd have a lot of trouble otherwise
because you'd have to protect the .smokes vector every time you access it
because there could be another thread launched in background
that modifies it
so I would advise to use this approach
I would like to have it like that but it's the same with the early return principle ๐
the whole program is designed around a single thread
I don't even really understand it because I didn't write it
okay okay
so your best course is to write it like this^
utilizing the threads only to run an algorithm in parallel
inside one function of the main thread
thanks for the help
@hexed loom Has your question been resolved? If so, run !solved :)
also small note, multithreading is not a magic pill to make things faster
also is this profile made in debug or release cause that matters as well
debug
I am trying to use the par_for_each method pawel wrote
however I am having some issues using it
my thread pool is a unique pointer
I don't think I can then pass that into the method?
it doesnt even have to be a unique pointer i dont think
no it does
otherwise I get this error
@burnt nova would you know how I can pass the unique_pointer thread_pool into the function?
yeah, then any profiling you did is basically useless
either by reference or pass ownership with std::move
nvm it's available in the same scope, I dont even have to pass it
par_for_each(tanks, &set_routes);
Is this not how I should pass the function?
ah Game::set_routes
For real though in the future make profiles in release
Debug ones are basically useless
okay
But yeah it's indeed game::set_routes
Though I'd be somewhat supriced if that just works
that part works
For some reason though, even though THREAD_AMOUNT and thread_pool are both in the same Tmpl8 namespace, I still get errors
when I control click them they go to the right thing
and I can't seem to be able to pass them to the function directly
what is the first error that is actually thrown in the output?
cause the error list is nice, but a bit flawed cause it isn't ordered
I get no output window, these are build errors
unless I dont understand what you mean with output window
so that is the std::min one?
yes
because auto num_threads = std::min(THREAD_AMOUNT, len);
THREAD_AMOUNT doesnt exist?
would it give that error?
nah, lets fix the first one first
that doesnt seem logical
auto len = range.size();
right ๐ค
template <typename Range, typename Func> // Template nodig voor een soort van "auto" types
void parallel_for_each(Range& range, Func func) {
auto len = range.size();
auto num_threads = std::min(THREAD_AMOUNT, len);
auto step = len / num_threads;
// Loopt over het aantal threads dat we willen maken
for (size_t i = 0; i < len; i += step)
thread_pool.enqueue([i, step, &func] {
auto curr = std::next(std::begin(range), i);
auto end = std::next(curr, step);
for (; curr != end; ++curr)
func(*curr); // * is nodig omdat curr een iterator is
});
}
yeah that is a size_t
yea that fixed that error
thread_pool however doesnt exist, probably because it is a unique_ptr
again always work on the first error first
alright
what is that range can't be brought in hat is true as you do not capture it
The first one is the thread_pool one
right
rightr where is threadpool defined?
In game.h
std::unique_ptr<ThreadPool> thread_pool;
and then in game.cpp I do
thread_pool = std::make_unique<ThreadPool>(THREAD_AMOUNT);
is that part of the game class?
this one is in a different class
but same namespace
right then yeah, you don't have access to it
I see
can I make it one?
you shouldn't
okay so I should pass it
non constant globals are basically always a bad idea
just pass it in as an argument for the paralled_for_each realy
Alright
as a std::unique_ptr<ThreadPool>& or just a Threadpool&, and then dereference the unique_ptr when calling it
ofcourse
also a small tip in general I would reccomend making comments in english over dutch
it's a bit more standard
yea I usually do
But for some reason they wanted it in dutch this time
Okay that fixed the thread_pool bug
odd, is this for buas?
but the range one is still here
[i, step, &func] vs [i, step, &func, &range]
what's that?
yeah that is cause it's not captured in the lambda
breda university of applied sciences
I just regoncnized the template and they generally use it for first year and entrance exams
and the teacher that made that template works at BUAS
cool
Yes that fixed that error
but now a new one
what exactly causes that error?
good!
with part of the tanks list
but that function doesn't take any arguments
so I'll need to change it
hm I changed it
void Tmpl8::Game::set_routes(vector<Tank>& tanks)
{
if (frame_count == 0)
{
for (Tank& t : tanks)
{
t.set_route(background_terrain.get_route(t, t.target));
}
}
}
but I still get the error
I think wrong type
yea I dont get this one
it should be able to call the method
oh wait
I completely read the thing wrong
no it's still not working
void Game::set_route(Tank& tank)
{
if (frame_count == 0)
{
tank.set_route(background_terrain.get_route(tank, tank.target));
}
}
1>D:\Programming\School\periode-2---optimalisatie-pepijn-s\game.cpp(168,60): message : see reference to function template instantiation 'void Tmpl8::parallel_for_each<std::vector<Tmpl8::Tank,std::allocator<Tmpl8::Tank>>,void(__cdecl Tmpl8::Game::* )(Tmpl8::Tank &)>(Tmpl8::ThreadPool &,Range &,Func)' being compiled
1> with
1> [
1> Range=std::vector<Tmpl8::Tank,std::allocator<Tmpl8::Tank>>,
1> Func=void (__cdecl Tmpl8::Game::* )(Tmpl8::Tank &)
1> ]
1>grid.cpp
@burnt nova do you see what the problem is?
I would like to see what curr is but I can't debug if it's a build error
I think the issue is with how I am passing the method
This question thread is being automatically closed. If your question is not answered feel free to bump the post or re-ask. Take a look at !howto ask for tips on improving your question.