This guide helps you migrate parallel code from OpenMP to dispenso. Dispenso offers several advantages over OpenMP for many use cases, including better nested parallelism, explicit thread pool control, and sanitizer-clean code.
Why Migrate?
| Aspect | OpenMP | Dispenso |
| Nested parallelism | Can cause thread explosion | Work-stealing prevents oversubscription |
| Thread pool control | Implicit, global | Explicit, multiple pools supported |
| Sanitizer support | Often problematic with TSAN | Clean with ASAN/TSAN |
| Portability | Requires compiler support | Pure C++14, any compiler |
| Futures | Not available | Full futures API |
| Task graphs | Limited | Rich graph support with partial re-execution |
Quick Reference
Basic Parallel For
OpenMP
#pragma omp parallel for
for (int i = 0; i < N; ++i) {
process(data[i]);
}
Dispenso
dispenso::parallel_for(0, N, [&](size_t i) {
process(data[i]);
});
Parallel For with Reduction
OpenMP
double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < N; ++i) {
sum += compute(data[i]);
}
Dispenso
Dispenso doesn't have built-in reduction syntax, but you can achieve the same result with thread-local accumulators:
std::atomic<double> sum{0.0};
dispenso::parallel_for(0, N, [&](size_t i) {
double val = compute(data[i]);
double expected = sum.load();
while (!sum.compare_exchange_weak(expected, expected + val)) {}
});
For better performance with many updates, use chunked iteration with local accumulators:
#include <mutex>
double sum = 0.0;
std::mutex sumMutex;
dispenso::parallel_for(
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kStatic),
[&](size_t begin, size_t end) {
double localSum = 0.0;
for (size_t i = begin; i < end; ++i) {
localSum += compute(data[i]);
}
std::lock_guard<std::mutex> lock(sumMutex);
sum += localSum;
});
Or use the state-per-thread overload of parallel_for:
std::vector<double> partialSums;
dispenso::parallel_for(
partialSums,
[]() { return 0.0; },
size_t{0}, static_cast<size_t>(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;
}
Nested Parallel Loops
This is where dispenso shines. OpenMP can create exponentially many threads with nested parallel regions, while dispenso's work-stealing handles this gracefully.
OpenMP (Problematic)
#pragma omp parallel for
for (int i = 0; i < M; ++i) {
#pragma omp parallel for
for (int j = 0; j < N; ++j) {
process(i, j);
}
}
Dispenso (Safe)
dispenso::parallel_for(0, M, [&](size_t i) {
dispenso::parallel_for(0, N, [&](size_t j) {
process(i, j);
});
});
With dispenso, the total number of threads is bounded by the thread pool size, regardless of nesting depth.
Critical Sections
OpenMP
#pragma omp parallel for
for (int i = 0; i < N; ++i) {
double val = compute(data[i]);
#pragma omp critical
{
results.push_back(val);
}
}
Dispenso
#include <mutex>
std::mutex resultsMutex;
dispenso::parallel_for(0, N, [&](size_t i) {
double val = compute(data[i]);
std::lock_guard<std::mutex> lock(resultsMutex);
results.push_back(val);
});
Or use dispenso::ConcurrentVector to avoid locking entirely:
dispenso::parallel_for(0, N, [&](size_t i) {
double val = compute(data[i]);
});
iterator push_back(const T &val)
Task Parallelism
OpenMP
#pragma omp parallel
{
#pragma omp single
{
#pragma omp task
taskA();
#pragma omp task
taskB();
#pragma omp taskwait
#pragma omp task
taskC();
}
}
Dispenso
tasks.schedule(taskA);
tasks.schedule(taskB);
tasks.wait();
tasks.schedule(taskC);
Controlling Thread Count
OpenMP
omp_set_num_threads(4);
#pragma omp parallel for num_threads(4)
Dispenso
Create a thread pool with the desired number of threads:
dispenso::parallel_for(pool, 0, N, [&](size_t i) {
process(data[i]);
});
Or use ParForOptions to limit parallelism:
dispenso::parallel_for(options, 0, N, [&](size_t i) {
process(data[i]);
});
Conditional Parallelism
OpenMP
#pragma omp parallel for if(N > 1000)
for (int i = 0; i < N; ++i) {
process(data[i]);
}
Dispenso
if (N > 1000) {
dispenso::parallel_for(0, N, [&](size_t i) {
process(data[i]);
});
} else {
for (size_t i = 0; i < N; ++i) {
process(data[i]);
}
}
Or use minItemsPerChunk to let dispenso decide:
dispenso::parallel_for(options, 0, N, [&](size_t i) {
process(data[i]);
});
uint32_t minItemsPerChunk
Static vs Dynamic Scheduling
OpenMP
#pragma omp parallel for schedule(static)
for (int i = 0; i < N; ++i) { ... }
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < N; ++i) { ... }
Dispenso
dispenso::parallel_for(
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kStatic),
[&](size_t begin, size_t end) {
for (size_t i = begin; i < end; ++i) {
process(data[i]);
}
});
dispenso::makeChunkedRange(0, N, dispenso::ParForChunking::kAuto),
[&](size_t begin, size_t end) {
for (size_t i = begin; i < end; ++i) {
process(data[i]);
}
});
void parallel_for(TaskSetT &taskSet, StateContainer &states, const StateGen &defaultState, const ChunkedRange< IntegerT > &range, F &&f, ParForOptions options={})
Thread-Local Storage
OpenMP
#pragma omp threadprivate(myThreadLocalVar)
int myThreadLocalVar;
#pragma omp parallel
{
myThreadLocalVar = omp_get_thread_num();
}
Dispenso
Use C++11 thread_local or the state-per-thread parallel_for overload:
thread_local int myThreadLocalVar;
dispenso::parallel_for(0, N, [&](size_t i) {
});
std::vector<MyState> states;
dispenso::parallel_for(
states,
[]() { return MyState{}; },
0, N,
[&](MyState& state, size_t begin, size_t end) {
});
Common Pitfalls When Migrating
1. Lambda Captures
OpenMP uses shared variables by default. With dispenso, be explicit about captures:
int x = 0;
#pragma omp parallel for
for (int i = 0; i < N; ++i) {
}
int x = 0;
dispenso::parallel_for(0, N, [&x](size_t i) {
});
2. Index Types
OpenMP typically uses int. Dispenso uses size_t:
#pragma omp parallel for
for (int i = 0; i < N; ++i) { ... }
dispenso::parallel_for(size_t{0}, static_cast<size_t>(N), [&](size_t i) {
});
3. Return Values
OpenMP parallel regions don't return values. With dispenso futures, you can:
auto future = dispenso::async([]() {
return expensiveComputation();
});
int result = future.get();
Performance Tips
- Use chunked ranges for better cache locality when iteration order doesn't matter
- Avoid over-synchronization - dispenso's
ConcurrentVector is often faster than mutex-protected std::vector
- Reuse thread pools - creating pools is expensive; create once and reuse
- Consider static chunking for uniform workloads, auto chunking for variable workloads
Further Reading