-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
187: Parallel Algorithms with the Execution Policy (C++17)
I see this all the time when developers first encounter C++17's parallel algorithms: they treat std::execution::par like a "magic turbo button." The assumption is that by simply inserting that policy as the first argument to std::sort or std::for_each, the code will automatically run faster across all CPU cores regardless of the workload.
The "Free Speed" Fallacy
The reality is that parallelism isn't free; it has a cost. Launching threads, coordinating their execution, and managing cache contention take time. If your dataset is small, or if the operation you're performing inside the loop is trivial, the overhead of managing the parallelism will actually make your code slower than the sequential version.
#include <vector>
#include <algorithm>
#include <execution>
#include <iostream>
void trivial_work() {
std::vector<int> data(100, 1); // Tiny dataset
// You might think this is "faster"
std::for_each(std::execution::par, data.begin(), data.end(), [](int& n) {
n++;
});
}
In the example above, incrementing an integer is a lightning-fast operation. The time it takes for the C++ runtime to partition the vector and assign tasks to different cores is orders of magnitude longer than the actual work of incrementing 100 integers. I've seen benchmarks where the parallel version is 10x slower than the sequential one for small inputs. Use parallelism when the "work per element" is high or the "number of elements" is massive.
Thread Safety is Now Your Problem
The second part of the misconception is thinking that std::execution::par handles the synchronization for you. It doesn't. The algorithm handles the distribution of work, but it does not protect your data. If your lambda captures a variable by reference and modifies it, you've just introduced a data race.
int total = 0;
std::vector<int> data(10000, 1);
// DANGER: This is a data race!
std::for_each(std::execution::par, data.begin(), data.end(), [&](int n) {
total += n;
});
In a sequential std::for_each, this is perfectly fine. In a parallel one, multiple threads will attempt to read, increment, and write to total simultaneously. You'll end up with a final value that is unpredictably lower than 10,000 because updates are being lost. To fix this, you'd need a std::atomic<int> or, better yet, use std::reduce which is designed specifically for this pattern.
Picking the Right Policy for the Workload
C++17 provides four execution policies. Choosing the right one is about communicating your intentions to the compiler and the hardware.
std::execution::seq: Forces sequential execution. Use this if you want to explicitly disable parallelism for a specific call to avoid the overhead I mentioned earlier.std::execution::par: Allows the implementation to run the algorithm in parallel on multiple threads. The order of execution is not guaranteed.std::execution::par_unseq: This is "parallel and vectorized." It tells the compiler that the work can be done in parallel and that it's safe to use SIMD (Single Instruction, Multiple Data) instructions. This is the fastest, but the most restrictive; you cannot use synchronization primitives (like mutexes) inside the lambda because it could lead to deadlocks during vectorization.std::execution::unseq: Only vectorization, no multi-threading.
My rule of thumb? Start with seq. If it's too slow and your dataset is large, try par. Only move to par_unseq if you've profiled your code and you know your operation is "embarrassingly parallel" and free of any synchronization requirements. And remember, depending on your compiler (like GCC), you might need to link against an external library like Intel TBB (Threading Building Blocks) to actually see the parallel gains.
📋 Practical Task
Exercise: Parallelizing a Large-Scale Coordinate Distance Summation
You are tasked with calculating the total distance of a set of 10 million 2D points from the origin (0,0). The current implementation is sequential and too slow for the production pipeline.
Requirements:
- Create a
std::vectorof a simplePointstruct (containingdouble x, y) with 10,000,000 elements, filled with random values. - Implement a function that calculates the sum of the distances using
std::sqrt(x*x + y*y). - First, implement this using
std::for_eachwithstd::execution::par. You must use astd::atomic<double>(or a similar thread-safe mechanism) to accumulate the total to avoid the data races discussed in the lesson. - Then, refactor the code to use
std::transform_reducewithstd::execution::par. This is the "correct" C++17 way to handle this pattern as it avoids the contention caused by an atomic variable. - Print the results of both methods to ensure they match.
There are no comments for now.