This guide helps you migrate parallel code from Intel Threading Building Blocks (TBB) to dispenso. While TBB has more features overall, dispenso offers advantages in several areas and provides a simpler, more focused API.
Why Migrate?
| Aspect | TBB | Dispenso |
| Sanitizer support | Often problematic with ASAN/TSAN | Clean with all sanitizers |
| Futures | Not available | Full std::experimental::future-like API |
| API complexity | Large, complex API | Focused, simpler API |
| Dependencies | Heavy library | Minimal dependencies |
| Non-Intel hardware | May not be optimized | Platform-neutral implementation |
| Nested parallelism | Good | Excellent (work-stealing optimized) |
Quick Reference
Parallel For with Index
TBB
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
tbb::parallel_for(tbb::blocked_range<size_t>(0, N),
[&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
process(data[i]);
}
});
Dispenso
dispenso::parallel_for(0, N, [&](size_t i) {
process(data[i]);
});
dispenso::parallel_for(
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kAuto),
[&](size_t begin, size_t end) {
for (size_t i = begin; i < end; ++i) {
process(data[i]);
}
});
Parallel Reduce
TBB
#include <tbb/parallel_reduce.h>
#include <tbb/blocked_range.h>
double sum = tbb::parallel_reduce(
tbb::blocked_range<size_t>(0, N),
0.0,
[&](const tbb::blocked_range<size_t>& range, double init) {
for (size_t i = range.begin(); i < range.end(); ++i) {
init += compute(data[i]);
}
return init;
},
std::plus<double>()
);
Dispenso
Use the state-per-thread parallel_for overload:
std::vector<double> partialSums;
dispenso::parallel_for(
partialSums,
[]() { return 0.0; },
size_t{0}, N,
[&](double& localSum, size_t begin, size_t end) {
for (size_t i = begin; i < end; ++i) {
localSum += compute(data[i]);
}
});
double sum = 0.0;
for (double partial : partialSums) {
sum += partial;
}
Parallel For Each
TBB
#include <tbb/parallel_for_each.h>
std::vector<Item> items;
tbb::parallel_for_each(items.begin(), items.end(), [](Item& item) {
process(item);
});
Dispenso
std::vector<Item> items;
dispenso::for_each(items.begin(), items.end(), [](Item& item) {
process(item);
});
Task Groups
TBB
#include <tbb/task_group.h>
tbb::task_group tg;
tg.run([]{ taskA(); });
tg.run([]{ taskB(); });
tg.wait();
tg.run([]{ taskC(); });
tg.wait();
Dispenso
tasks.schedule([]{ taskA(); });
tasks.schedule([]{ taskB(); });
tasks.wait();
tasks.schedule([]{ taskC(); });
tasks.wait();
For recursive task parallelism (where tasks spawn more tasks), use ConcurrentTaskSet:
if (depth == 0) return;
tasks.
schedule([&tasks, depth]{ recursiveWork(tasks, depth - 1); });
tasks.
schedule([&tasks, depth]{ recursiveWork(tasks, depth - 1); });
}
recursiveWork(tasks, 10);
void schedule(F &&f, bool skipRecheck=false, float poolRecursiveLoadFactor=kDefaultPoolRecursiveLoadFactor)
DISPENSO_DLL_ACCESS bool wait()
Concurrent Vector
TBB
#include <tbb/concurrent_vector.h>
tbb::concurrent_vector<int> vec;
tbb::parallel_for(size_t(0), N, [&](size_t i) {
vec.push_back(compute(i));
});
for (const auto& val : vec) {
use(val);
}
Dispenso
Dispenso's ConcurrentVector has a superset of TBB's API:
dispenso::parallel_for(size_t{0}, N, [&](size_t i) {
vec.push_back(compute(i));
});
for (const auto& val : vec) {
use(val);
}
Additional dispenso features:
vec.grow_by(100);
vec.grow_by_generator(100, [i = 0]() mutable { return i++; });
Task Arenas / Thread Pool Control
TBB
#include <tbb/task_arena.h>
#include <tbb/global_control.h>
tbb::global_control gc(tbb::global_control::max_allowed_parallelism, 4);
tbb::task_arena arena(4);
arena.execute([&] {
tbb::parallel_for(...);
});
Dispenso
Create explicit thread pools:
dispenso::parallel_for(pool, 0, N, [&](size_t i) {
process(data[i]);
});
Multiple pools can coexist for different workloads:
dispenso::parallel_for(computePool, 0, N, compute);
dispenso::parallel_for(ioPool, 0, M, ioWork);
Flow Graphs
TBB
#include <tbb/flow_graph.h>
tbb::flow::graph g;
tbb::flow::function_node<int, int> nodeA(g, tbb::flow::unlimited,
[](int v) { return processA(v); });
tbb::flow::function_node<int, int> nodeB(g, tbb::flow::unlimited,
[](int v) { return processB(v); });
tbb::flow::make_edge(nodeA, nodeB);
nodeA.try_put(input);
g.wait_for_all();
Dispenso
Dispenso's Graph is optimized for task DAGs with potential partial re-execution:
auto& nodeA = graph.
addNode([]{ processA(); });
auto& nodeB = graph.
addNode([]{ processB(); });
auto& nodeC = graph.
addNode([]{ processC(); });
nodeB.dependsOn(nodeA);
nodeC.dependsOn(nodeA);
dispenso::execute(graph, dispenso::globalThreadPool());
nodeA.setIncomplete();
dispenso::execute(graph, dispenso::globalThreadPool());
Futures (Dispenso Advantage)
TBB doesn't have a futures interface. Dispenso provides one:
return expensiveComputation();
});
int result = future.
get();
auto future2 = dispenso::async([]{ return 42; })
.then([](int x) { return x * 2; })
.then([](int x) { return std::to_string(x); });
std::string result = future2.get();
return std::get<0>(tuple).get() + std::get<1>(tuple).get();
});
const Result & get() const
Future< detail::ResultOf< F, Args... > > async(std::launch policy, F &&f, Args &&... args)
Future< std::vector< typename std::iterator_traits< InputIt >::value_type > > when_all(InputIt first, InputIt last)
Pipelines
TBB
#include <tbb/pipeline.h>
tbb::parallel_pipeline(
maxTokens,
tbb::make_filter<void, Data>(tbb::filter::serial_in_order,
[&](tbb::flow_control& fc) -> Data {
if (done) { fc.stop(); return {}; }
return readInput();
}) &
tbb::make_filter<Data, Data>(tbb::filter::parallel,
[](Data d) { return process(d); }) &
tbb::make_filter<Data, void>(tbb::filter::serial_in_order,
[](Data d) { writeOutput(d); })
);
Dispenso
Dispenso pipelines are simpler to construct:
dispenso::pipeline(
dispenso::globalThreadPool(),
dispenso::stage([]() -> std::optional<Data> {
if (done) return std::nullopt;
return readInput();
}, 1),
dispenso::stage([](Data d) { return process(d); }, 0),
dispenso::stage([](Data d) { writeOutput(d); }, 1)
);
Partitioners / Chunking
TBB
tbb::parallel_for(range, body, tbb::auto_partitioner());
tbb::parallel_for(range, body, tbb::static_partitioner());
tbb::affinity_partitioner ap;
tbb::parallel_for(range, body, ap);
Dispenso
dispenso::parallel_for(
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kAuto),
body);
dispenso::parallel_for(
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kStatic),
body);
Spin Mutexes
TBB
#include <tbb/spin_mutex.h>
tbb::spin_mutex mutex;
{
tbb::spin_mutex::scoped_lock lock(mutex);
}
Dispenso
#include <mutex>
std::mutex mutex;
{
std::lock_guard<std::mutex> lock(mutex);
}
{
dispenso::RWLock::ReadGuard rlock(rwLock);
}
{
dispenso::RWLock::WriteGuard wlock(rwLock);
}
Common Migration Patterns
Pattern 1: Replace blocked_range with makeChunkedRange
tbb::parallel_for(tbb::blocked_range<size_t>(0, N, grainSize), body);
dispenso::parallel_for(options,
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kStatic),
body);
uint32_t minItemsPerChunk
Pattern 2: Replace task_group recursion with ConcurrentTaskSet
void recursive(tbb::task_group& tg, int depth) {
if (depth == 0) return;
tg.run([&tg, depth]{ recursive(tg, depth-1); });
tg.run([&tg, depth]{ recursive(tg, depth-1); });
}
if (depth == 0) return;
tasks.
schedule([&tasks, depth]{ recursive(tasks, depth-1); });
tasks.
schedule([&tasks, depth]{ recursive(tasks, depth-1); });
}
Pattern 3: Combining parallel_for with futures
auto future = dispenso::async([&]() {
dispenso::parallel_for(0, N, [&](size_t i) {
process(data[i]);
});
return computeResult(data);
});
auto result = future.
get();
Performance Considerations
- Dispenso is faster for nested loops - TBB's nested parallelism can have higher overhead; dispenso's work-stealing is optimized for this case
- Dispenso has lower overhead for small loops - simpler scheduling means less overhead for fine-grained parallelism
- TBB may be faster for very large, uniform workloads - TBB's cache-affinity partitioner can help in specific scenarios
- Use appropriate chunking -
kStatic for uniform work, kAuto for variable work
- Reuse pools and TaskSets - creation has overhead; reuse when possible
Further Reading