在处理器核心数量快速增长而单核性能提升放缓的今天,并行计算已成为提升程序性能的关键路径。作为系统级编程语言的代表,C++在标准库中逐步引入了完整的并行计算支持,形成了从算法到底层硬件的完整加速方案。
C++17是一个重要的分水岭,它首次将并行执行策略纳入标准库。这种设计允许开发者在不重写算法逻辑的情况下,仅通过添加执行策略参数就能实现并行化。例如,传统的std::sort算法可以通过添加std::execution::par参数自动变为并行版本:
cpp复制std::vector<int> data = {...};
// 传统串行排序
std::sort(data.begin(), data.end());
// 并行版本(C++17起)
std::sort(std::execution::par, data.begin(), data.end());
C++20引入的std::ranges进一步简化了这种并行化操作。ranges提供了更现代的接口,消除了显式的begin/end迭代器对,使代码更简洁:
cpp复制namespace rng = std::ranges;
rng::sort(std::execution::par, data);
这种演进背后的核心思想是:将并行计算的复杂性从开发者转移到标准库实现者。开发者只需关注算法逻辑,而线程管理、任务分配等底层细节由标准库处理。
C++标准定义了三种主要的执行策略,每种策略对应不同的并行化方式:
seq(顺序执行):
std::execution::seqpar(并行执行):
std::execution::parpar_unseq(并行+向量化):
std::execution::par_unseq这些策略通过标签分发机制实现。当调用算法时,编译器会根据传入的策略选择不同的实现路径。例如,std::for_each的可能实现伪代码如下:
cpp复制template<typename Policy, typename It, typename Func>
void for_each(Policy&& policy, It begin, It end, Func f) {
if constexpr (is_same_v<Policy, decltype(std::execution::par)>) {
// 并行实现
parallel_impl(begin, end, f);
} else {
// 串行实现
serial_impl(begin, end, f);
}
}
并行策略的高效性很大程度上依赖于底层的工作窃取(work-stealing)调度器。这种调度器的核心工作原理是:
现代实现(如MSVC和libstdc++)通常使用线程池来避免频繁创建销毁线程的开销。线程池在程序启动时初始化,在执行并行算法时重用这些线程。
par_unseq策略的特殊之处在于它允许编译器进行向量化优化。现代CPU的SIMD(单指令多数据)指令集可以同时对多个数据执行相同操作:
例如,一个简单的向量加法在开启向量化后,编译器可能生成如下汇编代码:
asm复制; 未向量化版本
addss xmm0, xmm1 ; 单精度标量加法
; AVX向量化版本
vaddps ymm0, ymm1, ymm2 ; 同时执行8个float加法
并行算法的性能不仅取决于计算并行度,还受内存访问模式影响。优秀的实现会考虑:
例如,在实现并行reduce时,可以为每个线程分配独立的累加变量,最后再合并结果:
cpp复制std::vector<float> partial_sums(num_threads);
parallel_for(0, num_threads, [&](int tid) {
float local_sum = 0;
for (auto it = begin + tid*chunk; it != begin + (tid+1)*chunk; ++it) {
local_sum += *it;
}
partial_sums[tid] = local_sum;
});
float total = std::reduce(partial_sums.begin(), partial_sums.end());
并行化并非总是带来性能提升,需要考虑以下因素:
| 因素 | 适合并行化 | 不适合并行化 |
|---|---|---|
| 数据规模 | >10,000元素 | <1,000元素 |
| 计算密度 | 每个元素计算耗时>100ns | 简单操作(如+/*) |
| 内存访问 | 顺序访问模式 | 随机访问模式 |
| 任务类型 | 独立无状态操作 | 有复杂依赖关系 |
经验法则:先测量串行版本性能,如果单次算法执行时间超过1ms,才考虑并行化。
可靠的性能测试需要注意:
示例测试代码框架:
cpp复制auto benchmark = [](auto policy, auto&& algo) {
std::vector<int> data(1'000'000);
std::iota(data.begin(), data.end(), 0);
// 预热
for (int i = 0; i < 3; ++i) {
algo(policy, data.begin(), data.end());
}
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10; ++i) {
algo(policy, data.begin(), data.end());
}
auto end = std::chrono::high_resolution_clock::now();
return (end - start) / 10;
};
auto seq_time = benchmark(std::execution::seq, std::sort<decltype(data.begin())>);
auto par_time = benchmark(std::execution::par, std::sort<decltype(data.begin())>);
问题1:并行算法比串行更慢
if(data.size() > threshold) use_parallel()问题2:结果不一致
问题3:异常处理困难
try-catch包围整个并行区域标准库算法可能无法满足所有需求,这时可以基于执行策略实现自定义并行算法。基本模式如下:
cpp复制template<typename ExecutionPolicy, typename Iterator, typename Func>
void parallel_algorithm(ExecutionPolicy&& policy, Iterator begin, Iterator end, Func f) {
if constexpr (std::is_execution_policy_v<std::decay_t<ExecutionPolicy>>) {
// 并行实现
auto num_workers = std::thread::hardware_concurrency();
std::vector<std::thread> workers;
auto chunk_size = std::distance(begin, end) / num_workers;
for (unsigned i = 0; i < num_workers; ++i) {
workers.emplace_back([=, &f] {
auto first = begin + i * chunk_size;
auto last = (i == num_workers - 1) ? end : first + chunk_size;
for (auto it = first; it != last; ++it) {
f(*it);
}
});
}
for (auto& w : workers) w.join();
} else {
// 串行回退
for (auto it = begin; it != end; ++it) {
f(*it);
}
}
}
C++23开始探索与GPU等加速器的集成。虽然标准库尚未直接支持,但可以通过以下方式桥接:
std::execution::par_unseq策略示例使用OpenMP加速的transform:
cpp复制std::vector<float> parallel_transform(const std::vector<float>& input) {
std::vector<float> output(input.size());
#pragma omp parallel for simd
for (size_t i = 0; i < input.size(); ++i) {
output[i] = std::sqrt(input[i]);
}
return output;
}
理想的并行任务应该足够大以分摊调度开销,又足够小以充分利用所有核心。可以通过以下方式优化:
示例动态分块实现:
cpp复制template<typename It, typename Func>
void dynamic_parallel_for(It begin, It end, Func f) {
auto total = std::distance(begin, end);
auto min_chunk = std::max(total / (4 * std::thread::hardware_concurrency()), 1);
std::atomic<size_t> next_idx{0};
std::vector<std::thread> workers;
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) {
workers.emplace_back([&] {
while (true) {
auto start = next_idx.fetch_add(min_chunk);
if (start >= total) break;
auto chunk_end = std::min(start + min_chunk, total);
auto it = begin + start;
auto end_it = begin + chunk_end;
for (; it != end_it; ++it) {
f(*it);
}
}
});
}
for (auto& w : workers) w.join();
}
并行算法的内存访问模式极大影响性能。优化策略包括:
示例缓存优化矩阵乘法:
cpp复制void parallel_matrix_multiply(const Matrix& a, const Matrix& b, Matrix& result) {
constexpr size_t block_size = 64 / sizeof(float); // 缓存行大小
#pragma omp parallel for collapse(2)
for (size_t i = 0; i < a.rows; i += block_size) {
for (size_t j = 0; j < b.cols; j += block_size) {
for (size_t k = 0; k < a.cols; k += block_size) {
// 处理块
for (size_t ii = i; ii < std::min(i + block_size, a.rows); ++ii) {
for (size_t kk = k; kk < std::min(k + block_size, a.cols); ++kk) {
auto r = a[ii][kk];
for (size_t jj = j; jj < std::min(j + block_size, b.cols); ++jj) {
result[ii][jj] += r * b[kk][jj];
}
}
}
}
}
}
}
C++标准委员会正在多个方向扩展并行计算支持:
这些特性将逐步纳入未来C++标准,使并行计算支持更加完善和强大。