Skip to content

Commit

Permalink
Add thread pool for multithreaded parsing support
Browse files Browse the repository at this point in the history
  • Loading branch information
MikePopoloski committed May 14, 2023
1 parent a36bbe5 commit 94c6c05
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 2 deletions.
10 changes: 10 additions & 0 deletions docs/command-line-ref.dox
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ Any provided positional arguments that have extensions matching <ext> will be
This option is typically used when projects have existing command files listing sources
that are not SystemVerilog code.

`-j,--threads <count>`

Controls the number of threads used for parallel compilation. slang will by default
use the number of threads supported by the system -- this can be set to some other
value to more specifically control the concurrency. Setting it to 1 will disable
the use of threading.

Note that multithreading only currently applies to the parsing stage of compilation,
and that it is not supported when running with `--single-unit`

@section Actions

These options control what action the tool will perform when run.
Expand Down
3 changes: 3 additions & 0 deletions include/slang/driver/Driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ class SLANG_EXPORT Driver {
/// The maximum number of lexer errors that can be encountered before giving up.
std::optional<uint32_t> maxLexerErrors;

/// The number of threads to use for parsing.
std::optional<uint32_t> numThreads;

/// @}
/// @name Compilation
/// @{
Expand Down
145 changes: 145 additions & 0 deletions include/slang/util/ThreadPool.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//------------------------------------------------------------------------------
//! @file ThreadPool.h
//! @brief Lightweight thread pool class
//
// SPDX-FileCopyrightText: Michael Popoloski
// SPDX-License-Identifier: MIT
//------------------------------------------------------------------------------
#pragma once

#include <atomic>
#include <deque>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <type_traits>
#include <vector>

namespace slang {

/// A lightweight thread pool for running concurrent jobs.
class ThreadPool {
public:
/// @brief Constructs a new ThreadPool.
///
/// @param threadCount The number of threads to create in the pool. If zero (the default)
/// the number of threads will be set to the number of concurrent threads
/// supported by the system.
explicit ThreadPool(unsigned threadCount = 0) {
if (threadCount == 0) {
threadCount = std::thread::hardware_concurrency();
if (threadCount == 0)
threadCount = 1;
}

waiting = false;
running = true;

for (unsigned i = 0; i < threadCount; i++)
threads.emplace_back(&ThreadPool::worker, this);
}

/// Destroys the thread pool, blocking until all threads have exited.
~ThreadPool() {
waitForAll();

running = false;
{
std::unique_lock lock(mutex);
taskAvailable.notify_all();
}

for (auto& thread : threads)
thread.join();
}

/// @brief Pushes a new task into the pool for execution.
///
/// There is no way to wait for the pushed task to complete aside from
/// calling @a waitForAll and waiting for all tasks in the pool to complete.
template<typename TFunc, typename... TArgs>
void pushTask(TFunc&& task, TArgs&&... args) {
std::function<void()> func = std::bind(std::forward<TFunc>(task),
std::forward<TArgs>(args)...);
{
std::unique_lock lock(mutex);
tasks.emplace_back(std::move(func));
}

taskAvailable.notify_one();
}

/// @brief Submits a task into the pool for execution and returns a future
/// for tracking completion.
///
/// @returns A std::future that will eventually contain the result of the task.
template<typename TFunc, typename... TArgs,
typename TResult = std::invoke_result_t<std::decay_t<TFunc>, std::decay_t<TArgs>...>>
[[nodiscard]] std::future<TResult> submit(TFunc&& task, TArgs&&... args) {
std::function<TResult()> func = std::bind(std::forward<TFunc>(task),
std::forward<TArgs>(args)...);
auto taskPromise = std::make_shared<std::promise<TResult>>();

pushTask([func = std::move(func), taskPromise] {
try {
if constexpr (std::is_void_v<TResult>) {
std::invoke(func);
taskPromise->set_value();
}
else {
taskPromise->set_value(std::invoke(func));
}
}
catch (...) {
try {
taskPromise->set_exception(std::current_exception());
}
catch (...) {
}
}
});

return taskPromise->get_future();
}

/// Blocks the calling thread until all running tasks are complete.
void waitForAll() {
if (!waiting) {
waiting = true;
std::unique_lock lock(mutex);
taskDone.wait(lock, [this] { return tasks.empty(); });
waiting = false;
}
}

private:
void worker() {
while (running) {
std::unique_lock lock(mutex);
taskAvailable.wait(lock, [this] { return !tasks.empty() || !running; });

if (!tasks.empty()) {
auto task = std::move(tasks.front());
tasks.pop_front();
lock.unlock();
task();
lock.lock();

if (waiting)
taskDone.notify_one();
}
}
}

std::mutex mutex;
std::deque<std::function<void()>> tasks;
std::atomic<bool> running;
std::atomic<bool> waiting;
std::condition_variable taskAvailable;
std::condition_variable taskDone;
std::vector<std::thread> threads;
};

} // namespace slang
24 changes: 22 additions & 2 deletions source/driver/Driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "slang/syntax/SyntaxPrinter.h"
#include "slang/syntax/SyntaxTree.h"
#include "slang/util/Random.h"
#include "slang/util/ThreadPool.h"

namespace fs = std::filesystem;

Expand Down Expand Up @@ -91,6 +92,8 @@ void Driver::addStandardArgs() {
"Maximum number of errors that can occur during lexing before the rest of the file "
"is skipped",
"<count>");
cmdLine.add("-j,--threads", options.numThreads,
"The number of threads to use to parallelize parsing", "<count>");

// Compilation
cmdLine.add("--max-hierarchy-depth", options.maxInstanceDepth,
Expand Down Expand Up @@ -530,12 +533,29 @@ bool Driver::parseAllSources() {
syntaxTrees.emplace_back(std::move(tree));
}
else {
for (const SourceBuffer& buffer : buffers) {
auto parse = [&](const SourceBuffer& buffer) {
auto tree = SyntaxTree::fromBuffer(buffer, sourceManager, optionBag);
if (onlyLint)
tree->isLibrary = true;

syntaxTrees.emplace_back(std::move(tree));
return tree;
};

// If there are enough buffers to parse and the user hasn't disabled
// the use of threads, do the parsing via a thread pool.
if (buffers.size() > 4 && options.numThreads != 1u) {
ThreadPool threadPool(options.numThreads.value_or(0u));
std::vector<std::future<std::shared_ptr<SyntaxTree>>> tasks;
for (auto& buffer : buffers)
tasks.emplace_back(threadPool.submit(parse, buffer));

threadPool.waitForAll();
for (auto& task : tasks)
syntaxTrees.emplace_back(std::move(task.get()));
}
else {
for (auto& buffer : buffers)
syntaxTrees.emplace_back(parse(buffer));
}
}

Expand Down

0 comments on commit 94c6c05

Please sign in to comment.