performance improvements to JobSystem
- reduce the number of calls to notify_one() and notify_all(). notify_one() is not only called when running a new job, and notify_all() only when a job finishes. - don't hold the condition lock while calling notify_*(), as it is not strictly needed, and because notify_*() can be very slow, there can be a lot of contention on this lock as a result; blocking the whole jobsystem thread pool. - add a new version of run() that takes an opaque thread id that can be retrieved from a job's execute function; this is especially intended to be used by parallel_for(); it's just a more efficient version of run() that avoids a hashmap lookup. Overall these change yield a significant performance boost: - running + waiting a job: +200% - running many jobs: +150% - running many jobs in parallel: +50%
This commit is contained in:
committed by
Mathias Agopian
parent
a8ace2891d
commit
a60fe41681
@@ -29,14 +29,15 @@ static constexpr bool DEBUG_FINISH_HANGS = false;
|
||||
#include <utils/JobSystem.h>
|
||||
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/debug.h>
|
||||
#include <utils/Log.h>
|
||||
#include <utils/memalign.h>
|
||||
#include <utils/ostream.h>
|
||||
#include <utils/Panic.h>
|
||||
#include <utils/Systrace.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
@@ -252,6 +253,7 @@ inline bool JobSystem::hasJobCompleted(JobSystem::Job const* job) noexcept {
|
||||
}
|
||||
|
||||
void JobSystem::wait(std::unique_lock<Mutex>& lock, Job* job) noexcept {
|
||||
HEAVY_SYSTRACE_CALL();
|
||||
if constexpr (!DEBUG_FINISH_HANGS) {
|
||||
mWaiterCondition.wait(lock);
|
||||
} else {
|
||||
@@ -289,27 +291,29 @@ void JobSystem::wait(std::unique_lock<Mutex>& lock, Job* job) noexcept {
|
||||
}
|
||||
|
||||
void JobSystem::wakeAll() noexcept {
|
||||
// wakeAll() is called when a job finishes (to wake up any thread that might be waiting on it)
|
||||
HEAVY_SYSTRACE_CALL();
|
||||
std::lock_guard<Mutex> const lock(mWaiterLock);
|
||||
mWaiterLock.lock();
|
||||
// this empty critical section is needed -- it guarantees that notify_all() happens
|
||||
// after the condition's variables are set.
|
||||
mWaiterLock.unlock();
|
||||
// notify_all() can be pretty slow, and it doesn't need to be inside the lock.
|
||||
mWaiterCondition.notify_all();
|
||||
}
|
||||
|
||||
void JobSystem::wake(size_t hint) noexcept {
|
||||
void JobSystem::wakeOne() noexcept {
|
||||
// wakeOne() is called when a new job is added to a queue
|
||||
HEAVY_SYSTRACE_CALL();
|
||||
std::lock_guard<Mutex> const lock(mWaiterLock);
|
||||
// this empty critical section is needed -- it guarantees that notify_all() happens
|
||||
mWaiterLock.lock();
|
||||
// this empty critical section is needed -- it guarantees that notify_one() happens
|
||||
// after the condition's variables are set.
|
||||
if (hint == 1) {
|
||||
mWaiterCondition.notify_one();
|
||||
} else {
|
||||
mWaiterCondition.notify_all();
|
||||
}
|
||||
mWaiterLock.unlock();
|
||||
// notify_one() can be pretty slow, and it doesn't need to be inside the lock.
|
||||
mWaiterCondition.notify_one();
|
||||
}
|
||||
|
||||
inline JobSystem::ThreadState& JobSystem::getState() noexcept {
|
||||
std::lock_guard<utils::Mutex> const lock(mThreadMapLock);
|
||||
std::lock_guard<Mutex> const lock(mThreadMapLock);
|
||||
auto iter = mThreadMap.find(std::this_thread::get_id());
|
||||
FILAMENT_CHECK_PRECONDITION(iter != mThreadMap.end()) << "This thread has not been adopted.";
|
||||
return *iter->second;
|
||||
@@ -331,7 +335,7 @@ void JobSystem::put(WorkQueue& workQueue, Job* job) noexcept {
|
||||
// But it's possible that the job has already been picked-up, so oldActiveJobs could be
|
||||
// negative for instance. We signal only if that's not the case.
|
||||
if (oldActiveJobs >= 0) {
|
||||
wake(oldActiveJobs + 1); // wake-up a thread if needed...
|
||||
wakeOne(); // wake-up a thread if needed...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,13 +351,9 @@ JobSystem::Job* JobSystem::pop(WorkQueue& workQueue) noexcept {
|
||||
// If our guess was wrong, i.e. we couldn't pick up a job (b/c our queue was empty), we
|
||||
// need to correct mActiveJobs.
|
||||
if (!job) {
|
||||
int32_t const oldActiveJobs = mActiveJobs.fetch_add(1, std::memory_order_relaxed);
|
||||
if (oldActiveJobs >= 0) {
|
||||
// And if there are some active jobs, then we need to wake someone up. We know it
|
||||
// can't be us, because we failed taking a job and we know another thread can't
|
||||
// have added one in our queue.
|
||||
wake(oldActiveJobs + 1); // wake-up a thread if needed...
|
||||
}
|
||||
// no need to wake someone else up because, we will go into job-stealing mode
|
||||
// immediately after this
|
||||
mActiveJobs.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
@@ -364,19 +364,14 @@ JobSystem::Job* JobSystem::steal(WorkQueue& workQueue) noexcept {
|
||||
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
|
||||
|
||||
size_t const index = workQueue.steal();
|
||||
assert(index <= MAX_JOB_COUNT);
|
||||
assert_invariant(index <= MAX_JOB_COUNT);
|
||||
Job* const job = !index ? nullptr : &mJobStorageBase[index - 1];
|
||||
|
||||
// If we failed taking a job, we need to correct mActiveJobs.
|
||||
if (!job) {
|
||||
int32_t const oldActiveJobs = mActiveJobs.fetch_add(1, std::memory_order_relaxed);
|
||||
if (oldActiveJobs >= 0) {
|
||||
// And if there are some active jobs, then we need to wake someone up. We know it
|
||||
// can't be us, because we failed taking a job and we know another thread can't
|
||||
// have added one in our queue.
|
||||
wake(oldActiveJobs + 1); // wake-up a thread if needed...
|
||||
}
|
||||
// If we failed taking a job, we need to correct mActiveJobs.
|
||||
mActiveJobs.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
@@ -430,7 +425,9 @@ bool JobSystem::execute(JobSystem::ThreadState& state) noexcept {
|
||||
|
||||
if (UTILS_LIKELY(job->function)) {
|
||||
HEAVY_SYSTRACE_NAME("job->function");
|
||||
job->id = std::distance(mThreadStates.data(), &state);
|
||||
job->function(job->storage, *this, job);
|
||||
job->id = invalidThreadId;
|
||||
}
|
||||
finish(job);
|
||||
}
|
||||
@@ -442,9 +439,10 @@ void JobSystem::loop(ThreadState* state) noexcept {
|
||||
setThreadPriority(Priority::DISPLAY);
|
||||
|
||||
// record our work queue
|
||||
mThreadMapLock.lock();
|
||||
std::unique_lock<Mutex> lock(mThreadMapLock);
|
||||
bool const inserted = mThreadMap.emplace(std::this_thread::get_id(), state).second;
|
||||
mThreadMapLock.unlock();
|
||||
lock.unlock();
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(inserted) << "This thread is already in a loop.";
|
||||
|
||||
// run our main loop...
|
||||
@@ -532,10 +530,6 @@ void JobSystem::release(JobSystem::Job*& job) noexcept {
|
||||
job = nullptr;
|
||||
}
|
||||
|
||||
void JobSystem::signal() noexcept {
|
||||
wakeAll();
|
||||
}
|
||||
|
||||
void JobSystem::run(Job*& job) noexcept {
|
||||
HEAVY_SYSTRACE_CALL();
|
||||
|
||||
@@ -547,6 +541,18 @@ void JobSystem::run(Job*& job) noexcept {
|
||||
job = nullptr;
|
||||
}
|
||||
|
||||
void JobSystem::run(Job*& job, uint8_t id) noexcept {
|
||||
HEAVY_SYSTRACE_CALL();
|
||||
|
||||
ThreadState& state = mThreadStates[id];
|
||||
assert_invariant(&state == &getState());
|
||||
|
||||
put(state.workQueue, job);
|
||||
|
||||
// after run() returns, the job is virtually invalid (it'll die on its own)
|
||||
job = nullptr;
|
||||
}
|
||||
|
||||
JobSystem::Job* JobSystem::runAndRetain(Job* job) noexcept {
|
||||
JobSystem::Job* retained = retain(job);
|
||||
run(job);
|
||||
@@ -591,6 +597,7 @@ void JobSystem::waitAndRelease(Job*& job) noexcept {
|
||||
}
|
||||
|
||||
void JobSystem::runAndWait(JobSystem::Job*& job) noexcept {
|
||||
SYSTRACE_CALL();
|
||||
runAndRetain(job);
|
||||
waitAndRelease(job);
|
||||
}
|
||||
@@ -598,7 +605,7 @@ void JobSystem::runAndWait(JobSystem::Job*& job) noexcept {
|
||||
void JobSystem::adopt() {
|
||||
const auto tid = std::this_thread::get_id();
|
||||
|
||||
std::unique_lock<utils::Mutex> lock(mThreadMapLock);
|
||||
std::unique_lock<Mutex> lock(mThreadMapLock);
|
||||
auto iter = mThreadMap.find(tid);
|
||||
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
|
||||
lock.unlock();
|
||||
@@ -631,7 +638,7 @@ void JobSystem::adopt() {
|
||||
|
||||
void JobSystem::emancipate() {
|
||||
const auto tid = std::this_thread::get_id();
|
||||
std::lock_guard<utils::Mutex> const lock(mThreadMapLock);
|
||||
std::unique_lock<Mutex> const lock(mThreadMapLock);
|
||||
auto iter = mThreadMap.find(tid);
|
||||
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
|
||||
FILAMENT_CHECK_PRECONDITION(state) << "this thread is not an adopted thread";
|
||||
|
||||
Reference in New Issue
Block a user