告别性能焦虑!C++17 并行算法从入门到精通

开发 后端
随着硬件性能的提升和C++标准的发展,并行算法必将在现代C++编程中发挥越来越重要的作用。掌握这项"神器",让我们的代码插上腾飞的翅膀! ​

"救命啊!!!" 小王抓着头发盯着屏幕,都快哭出来了,"这破数据怎么处理了一整天还在跑?我的周末要泡汤了!"

老张悠哉悠哉地端着他那标志性的"全宇宙最棒程序员"马克杯晃了过来,香浓的咖啡香气飘得整个办公室都是。"哟,遇到困难了?让我瞧瞧..." 他推了推那副程序员标配的黑框眼镜,"哦~这不就是那个传说中的千万级用户日志分析任务嘛!"

"可不是嘛!" 小王指着屏幕上密密麻麻的代码欲哭无泪,"我用了std::accumulate 来做统计,结果这程序跑得比蜗牛还慢,我都怀疑人生了!"

老张嘴角微微上扬,露出了一个"前辈高手"的神秘微笑:"年轻人,让我来告诉你一个改变你程序人生的秘密神器 - C++17的并行算法!它就像是给你的程序装上了火箭推进器,让数据处理飞起来!"

小王的眼睛一下子亮了起来,整个人都从椅子上弹了起来:"真的吗?快教教我!"

初识并行算法

老张推了推眼镜,露出高深莫测的微笑:"来来来,让我给你介绍一个编程界的超级英雄 - C++17的并行算法!"

"并行算法?听起来好像很厉害的样子!" 小王的眼睛里闪烁着求知的光芒。

"其实啊,这就像是给你的程序配备了一支超级战队!" 老张眨眨眼睛,"我先给你解释一下std::accumulate - 它就像是一个计数器,可以把一串数字都加起来。比如你要统计所有用户的消费总额,或者计算一组数据的总和,都可以用它。但它只能一个数一个数地慢慢加,就像是一个人在那里掰着手指头数数。"

"来看看这段魔法代码:"

// 以前只能一个人慢慢数砖头 🐌
// accumulate 会从头到尾遍历数据,把每个数都加到初始值(这里是0)上
auto sum = std::accumulate(data.begin(), data.end(), 0);

// 现在可以叫上所有小伙伴一起数!🚀
#include <execution>
auto sum = std::reduce(std::execution::par, data.begin(), data.end(), 0);

"就...就这么简单?感觉像在变魔术!" 小王目瞪口呆。

"没错!" 老张得意地晃了晃他的咖啡杯,"这就是现代 C++ 的魅力所在!只要加上std::execution::par 这个小魔法师 ,你的程序就能召唤出多个 CPU 核心一起并肩作战了!就像是给你的代码装上了涡轮增压器,嗖的一下就飞起来了!"

程序重新编译运行,这次的速度简直像是坐上了火箭,不到半小时就完成了原来需要一整天的工作。

"哇塞!这也太神奇了吧!" 小王激动地在椅子上转来转去,"感觉我的程序突然开挂了!"

老张笑着喝了口咖啡:"记住啊,这个魔法虽然强大,但也要看场合使用。数据太少的时候,反而会因为召唤'小伙伴'耽误时间。就像叫一群人来搬一块砖,光是组织大家就费劲了!" 

"这个道理我懂!" 小王若有所思地点点头,"就像打游戏,小怪用普攻就够了,只有打 BOSS 才需要放大招!" 

执行策略详解

"哇!太神奇了!" 小王兴奋地转着椅子,"那个par 是什么意思啊?"

"这个啊,是 parallel 的缩写,表示并行执行。" 老张解释道,"C++17给我们提供了三种执行策略:

  • seq - 就像你以前写的那样,按顺序执行
  • par - 并行执行,适合CPU密集型任务
  • par_unseq - 并行加向量化,适合简单的数值计算"

小王听得目瞪口呆:"所以说,处理不同的任务,就该选择不同的'战斗模式'咯?"

"没错!" 老张神秘地眨眨眼,"就像打游戏要看怪物选择武器一样,处理简单的小数据,就用seq 独行侠模式;遇到需要大量计算的复杂任务,就开启par 分身模式;如果是纯数值计算这种简单粗暴的活儿,那就直接上par_unseq 终极模式,让CPU的每个核心都嗨起来!" 

实战示例

"诶~老张,我这还有个需求..." 小王挠挠头,露出一副求知的表情,"要是我想对一大堆数据做批量计算,比如给所有商品打个折,有什么快速的方法吗?"

老张放下他那冒着热气的咖啡杯,眼睛里闪过一丝智慧的光芒:"这简单啊!就用咱们的并行版transform 呗!它就像是一个魔法复制机器 🪄,不但能复制,还能在复制的时候顺便改变数据。来看看这段魔法咒语:"

std::vector<double> prices{/* 这里塞满了商品价格 💰 */};
std::vector<double> discounted(prices.size());

// 召唤并行折扣魔法 ✨
std::transform(std::execution::par,
    prices.begin(), prices.end(),
    discounted.begin(),
    [](double price) { return price * 0.8; }  // 施展八折魔法 🎉
);

"看到没?" 老张得意地转了转他的魔法棒(其实是他的签字笔),"这段代码就像是召唤了一群小精灵,它们同时出动,每个小精灵负责处理一部分价格,刷刷刷~ 一眨眼的功夫,所有商品就都打好折扣啦!比你一个一个手动计算不知道快到哪里去了!"

小王的眼睛瞬间亮了起来:"哇塞!这也太方便了吧!感觉就像是给程序开了个外挂一样!"

"没错!" 老张笑着点点头,"而且这个魔法特别灵活,不光是打折,你想对数据做任何批量处理都可以,比如计算平方、求对数,甚至是更复杂的运算,通通都不在话下!就是要记得,这么强大的魔法,最好是在数据量比较大的时候再用,不然就有点大材小用啦~" 

小王兴奋地搓了搓手:"太棒了!这下我的数据处理要起飞咯~"

性能注意事项

"哎呀,等等!" 老张突然举起他那冒着热气的咖啡杯,露出一副"我要传授秘籍"的表情,"并行虽然是个好东西,但也不是什么时候都能派上用场的神器哦!"

小王正沉浸在并行的魔力中,听到这话立刻竖起了耳朵:"咦?这是为啥呀?"

老张神秘地笑了笑:"你想啊,就像组织一场派对 - 叫上三五好友一起玩还挺热闹的,但要是就吃一块小蛋糕,你还要发几十个邀请、等大家到齐,那不是搞得太隆重了嘛!并行也是一样的道理 - 启动线程要时间,线程之间互相打招呼也要时间,这些开销可不小呢!"

"啊!我懂了!" 小王恍然大悟地拍了下桌子,"就像叫一群朋友来搬一个小箱子,光是组织大家来就累死了!"

"聪明!" 老张赞许地点点头,"一般来说啊,除非你的数据量像天上的星星一样多(至少上万个吧),不然还真不如一个人慢慢来。毕竟摆好'独行侠'的架势,有时候反而比召唤一支'复仇者联盟'来得更实在呢!"

小王若有所思地摸着下巴:"这么说的话,我得好好掂量掂量什么时候该放大招了!"

"就是这个理儿!" 老张开心地喝了口咖啡,眼睛笑得像月牙儿一样,"记住啊,程序优化就像武功修炼,要讲究个'恰到好处'。有时候,简单朴实的单线程反而是最佳选择呢!" 

使用建议

"哎呀,差点忘了最重要的一点!" 老张突然转过身来,神秘兮兮地压低声音说,"要说并行计算的终极秘诀,那就是容器的选择特别重要!就像选武器一样,用vector 这种连续存储的容器,就像是挥舞一把锋利的宝剑,干脆利!但要是用list 这种到处都是指针的容器,那就像是在用一把生锈的钝刀,砍得你手都酸了~"

"原来如此!" 小王恍然大悟,赶紧掏出他那本写满笔记的小本本,生怕漏掉任何一个重要细节。

老张看着小王认真的样子,欣慰地笑了:"记住啊,写代码就像做菜,先把基本功练好才是王道。等你觉得程序慢得像蜗牛爬的时候,再考虑加上并行这个'秘制调料'也不迟!"

"好嘞!" 小王朝着老张的背影挥手致敬 🫡,心里暗暗发誓要把今天学到的并行魔法好好消化。转过头来,他的手指已经在键盘上飞舞起来,开始了代码重构的冒险之旅。

"等等!" 老张又探出头来,眨了眨眼睛,"记得多测试啊!并行计算就像是驾驭一匹烈马,看起来威风,但也要当心别被甩下来!"

小王竖起大拇指:"放心吧,老张!我一定会从简单开始,循序渐进地驯服这匹并行'烈马'的!"

小贴士 

(1) 记得包含<execution> 头文件

(2) 根据编译器可能需要链接并行库:

  • MSVC: 无需额外库
  • GCC: 需要-ltbb 或 OpenMP
  • Clang: 支持有限,可能需要 TBB

(3) 并行执行可能会改变元素处理顺序

(4) Lambda 表达式要保证线程安全

更多实战案例

"诶,老张!" 小王举手提问道,"我想知道除了刚才的那些,还有什么好用的并行算法呀?"

老张放下他那标志性的程序员马克杯,眼睛闪着智慧的光芒:"来来来,让我给你展示一些超级实用的并行魔法!"

基础操作篇

首先是一些常见的基础操作:

#include <execution>
#include <algorithm>
#include <vector>

// 准备一些测试数据 📊
std::vector<int> scores{
    /* 这里是学生成绩数据 */
}; 

// 1. 并行排序 - 给成绩单排序 📈
// 可以快速对大量成绩进行排序
std::sort(
    std::execution::par,  // 开启并行模式
    scores.begin(), 
    scores.end()
);

// 2. 并行查找及格的同学 🔍
auto pass_score = 60;
auto it = std::find_if(
    std::execution::par,
    scores.begin(), 
    scores.end(),
    [pass_score](int score) { 
        return score >= pass_score; 
    }
);

"哇塞!这也太方便了!" 小王惊叹道,"感觉代码一下子变得好简洁!"

老张喝了口咖啡,神秘地笑了笑:"这还不是最厉害的呢!来看看这些并行算法的'终极大招'!想象一下,你是个老师,要统计一个年级上千名学生里有多少个学霸,用以前的方法,怕是要数到头晕眼花。但有了并行版的count_if,就像是分身出好多个老师一起数,效率蹭蹭往上涨!"

"再比如啊," 老张眨眨眼继续说道,"有时候系统出故障,不小心把一些同学的分数记成负数了。要是一个一个改,那得改到什么时候去?但用并行版的replace_if,就像是召唤出一群小精灵,每个小精灵负责修改一部分数据,刷刷几下就把负分都变成0分啦!"

老张举起他那标志性的马克杯:"这就是并行算法的魅力 - 它不光能让你的代码看起来更优雅,还能真真切切地帮你节省时间。就像是给你的程序装上了一个'时间加速器' ,让繁重的工作变得轻松愉快!"

// 3. 并行统计 - 数一数优秀生有多少 🏆
auto excellent_count = std::count_if(
    std::execution::par,
    scores.begin(), 
    scores.end(),
    [](int score) { 
        return score >= 90;  // 90分以上就是优秀
    }
);

// 4. 并行数据修正 - 把负分都改成0分 ✨
std::replace_if(
    std::execution::par,
    scores.begin(), 
    scores.end(),
    [](int score) { 
        return score < 0;  // 找出负分
    }, 
    0// 替换为0分
);

小王听得入迷了:"这简直就像变魔术一样!以前要写好多复杂的多线程代码才能实现的功能,现在只要加个par 就搞定了!"

"没错!" 老张得意地说,"现代C++就是这么神奇,它把复杂的并行计算都包装得像施魔法一样简单。不过记住啊,这些魔法虽然强大,但也要在数据量够大的时候才能发挥威力。就像召唤神龙,要集齐七颗龙珠才行!"

高级操作篇

"哎呀,小王啊," 老张神秘兮兮地凑近了一点,"刚才那些都是基础操作,现在让我们来点更刺激的!"

小王立刻来了精神,眼睛闪闪发亮:"快说快说!"

"想象一下,你是个班主任" 老张眨眨眼继续说道,"现在要把A班和B班的成绩单合并,还得按分数排序。用以前的方法,怕是要对着两张表来回看,忙活半天。但有了并行版的merge,就像变魔术一样简单!"

// 两个班的成绩单准备好咯 📚
std::vector<int> class_a{/* A班的小可爱们的分数 */};
std::vector<int> class_b{/* B班的成绩单 */};

// 施展合并魔法! ✨
std::vector<int> merged(class_a.size() + class_b.size());
std::merge(
    std::execution::par,  // 召唤并行小精灵们
    class_a.begin(), class_a.end(),
    class_b.begin(), class_b.end(),
    merged.begin()
);

"但这还不是最厉害的呢!" 老张端起他那冒着热气的咖啡杯,"假设现在要计算期末总评,每科都有不同的权重。要是手算,准能算到头昏眼花。但有了并行版的inner_product,就像有一群小精灵在帮你计算一样!"

// 每科的权重都在这里 ⚖️
std::vector<double> weights{/* 各科目的分量 */};

// 召唤加权计算魔法! 🪄
auto weighted_sum = std::inner_product(
    std::execution::par,
    scores.begin(), scores.end(),
    weights.begin(),
    0.0  // 从0开始累加
);

"哦对了!" 老张突然想起什么似的一拍大腿,"还有个特别好玩的 - 要是想看看每个同学的成绩累计到他这里是多少分,用inclusive_scan 一下子就搞定了!就像是给成绩单施了个连加魔法!"

// 施展累计魔法! 📈
std::vector<int> running_total(scores.size());
std::inclusive_scan(
    std::execution::par,
    scores.begin(), scores.end(),
    running_total.begin()
);

小王听得如痴如醉:"这简直太神奇了!感觉整个人都升级了!"

老张得意地抿了口咖啡:"不过啊,这些魔法也是要看场合的。就像召唤神龙,数据太少的时候反而会浪费法力值。而且啊,最好用vector 这种整整齐齐的容器,不然就像是在杂乱的房间里施法,效果可就差远了!"

"明白!" 小王使劲点头,生怕漏掉任何细节,"这就是并行算法的终极奥义啊!"

"没错!" 老张笑着站起身来,"好好练习这些魔法,你也能成为并行世界的大法师!" 

小王看着老张远去的背影,心里暗暗发誓一定要把这些并行魔法练得炉火纯青。毕竟,谁不想让自己的代码插上翅膀,飞得更快呢? 

总结与展望 

通过这次并行算法的探索之旅,我们学到了:

(1) 并行算法的核心优势 

  • 充分利用多核CPU性能
  • 大幅提升数据处理速度
  • 代码简洁易读,使用方便

(2) 三种执行策略的应用场景 

  • seq: 适合小数据量顺序处理
  • par: 适合CPU密集型大数据任务
  • par_unseq: 适合简单的数值计算操作

(3) 使用注意事项

  • 数据量要足够大才值得并行
  • 优先使用连续存储的容器(如vector)
  • 确保并行操作的线程安全性
  • 根据实际性能测试选择最佳策略

(4) 常用并行算法

  • 基础算法: sort, find_if, count_if
  • 数值计算: reduce, transform
  • 高级操作: merge, inner_product, inclusive_scan

展望未来,随着硬件性能的提升和C++标准的发展,并行算法必将在现代C++编程中发挥越来越重要的作用。掌握这项"神器",让我们的代码插上腾飞的翅膀! 

责任编辑:赵宁宁 来源: everystep
相关推荐

2022-09-02 15:11:18

开发工具

2023-12-18 10:11:36

C++17C++代码

2010-11-08 10:20:18

2024-12-18 06:00:00

C++17C++

2023-09-16 18:54:38

Pythonfor循环

2010-02-06 15:31:18

ibmdwAndroid

2009-07-22 14:55:16

ibmdwAndroid

2017-05-09 08:48:44

机器学习

2016-12-08 22:39:40

Android

2022-06-10 08:17:52

HashMap链表红黑树

2012-02-29 00:49:06

Linux学习

2024-12-27 12:00:00

C++17枚举

2024-12-19 11:30:00

C++17CTAD代码

2024-02-26 08:52:20

Python传递函数参数参数传递类型

2009-07-03 18:49:00

网吧综合布线

2023-10-13 08:23:05

2024-12-20 18:00:00

C++折叠表达式C++17

2011-10-26 20:47:36

ssh 安全

2009-03-19 13:36:53

SSH安全通道远程

2017-01-09 09:34:03

Docker容器传统虚拟机
点赞
收藏

51CTO技术栈公众号