计算机系统与并行计算:任务分解、内存一致性与加速比
1. 为什么并行计算不是多开几个线程这么简单我见过太多人第一次接触并行计算时的反应既然一个核跑得慢那就开八个线程一起跑速度不就翻八倍了这个想法很符合直觉但现实往往很残酷——你写完多线程版本跑出来比单线程还慢或者偶尔快偶尔慢甚至结果偶尔还是错的。我自己第一次写并行程序就是这样四个线程跑一个求和任务结果比单线程慢了三倍当时盯着屏幕怀疑人生。问题出在哪并行计算从来不是把任务切碎丢给多个核心这么简单。它是一整套关于任务分解、数据划分、通信协调、同步互斥、性能建模的系统工程。你得先想清楚这个任务到底能不能并行并行之后各个部分怎么交换数据交换数据的代价会不会把并行省下来的时间全吃掉这些问题的答案决定了你写出来的程序是真正加速还是白忙一场。这篇文章我想把并行计算这套东西从头到尾讲透。从并行到底解决什么问题、有哪些常见的并行模型、内存一致性和同步这些容易踩坑的地方、再到实际写代码时怎么评估加速比和扩展性最后聊聊常见的性能陷阱。适合已经会写基本代码、但对为什么我的并行程序不加速一头雾水的读者也适合想系统补一下计算机系统里这块知识的同学。核心关键词是计算机系统和并行计算我会尽量用生活化的类比把抽象概念讲清楚同时保留足够的技术细节让你能真正上手。先说一个贯穿全文的基本判断并行计算的本质不是让多个核心同时干活而是在通信与计算的权衡中找到最优切分方式。记住这句话后面所有的内容都是它的展开。2. 并行计算的两条根本路线数据并行与任务并行2.1 从搬砖类比理解两种并行假设你要把一万块砖从A地搬到B地。有两种组织方式第一种你雇十个人每人负责搬一千块砖大家各搬各的互不干扰。这叫数据并行——同一套操作作用在数据的不同部分上。第二种你雇三个人一个人负责搬砖一个人负责砌墙一个人负责搅拌水泥。三个人做的是不同的工序擅长不同的活。这叫任务并行——把不同的功能模块交给不同的执行单元。这两种思路是并行计算的两条根本路线。数据并行更常见因为它的扩展性好数据量变大加人就行任务并行受限于工序数量你总不能把搬砖再拆成左手搬和右手搬那样协调成本比收益还大。实际系统里两者往往混合使用。比如一个图像处理流水线可能整体是任务并行的读图、滤波、编码三个阶段各跑各的但每个阶段内部又是数据并行的滤波阶段把图片切成多块同时处理。2.2 数据并行的两种切法数据并行往下再细分又有两种切法这个区别非常关键搞混了会写出性能很差的代码。按块切分把一万块砖分成十堆每堆连续的一千块。对应到数组就是把下标 0-999 给线程一1000-1999 给线程二依此类推。这种方式的好处是每个线程访问的内存是连续的缓存命中率高因为在计算机系统里缓存是按块加载的连续访问能把一整块缓存行用满。按轮转切分线程一拿第 0、10、20 块线程二拿第 1、11、21 块。这种方式乍看奇怪但在某些场景下有用比如当各个数据项的计算量差异很大时轮转切分能让负载更均匀。代价是内存访问不连续缓存效率下降可能反而更慢。我个人的经验是能用按块切分就用按块切分只有在负载严重不均、且计算量远大于内存访问开销时才考虑轮转或更复杂的调度。这句话背后是实打实的性能差异同一个矩阵运算切法不同跑出来的时间可能差两三倍。2.3 任务并行里的依赖关系任务并行的核心难点不是怎么分成多个任务而是任务之间有依赖谁先谁后。这就要提到**有向无环图DAG**这个模型每个任务是一个节点依赖关系是一条有向边边指向的任务必须等前面的任务完成才能开始。举个实际的例子。一个典型的推荐系统流水线可能是这样的读取用户数据无依赖读取物品数据无依赖特征拼接依赖 1 和 2模型推理依赖 3结果排序依赖 4写回存储依赖 5任务 1 和 2 可以并行3 之后基本是串行的。看到问题了吗这条流水线几乎没什么并行度因为越往后依赖越重。这时候硬拆任务并行没意义反而应该考虑第 3、4、5 步能不能做批处理式的数据并行——把多个用户的请求攒一批一起算让 GPU 或 SIMD 指令把它们同时处理掉。判断一个任务该用数据并行还是任务并行我的经验法则是看它的瓶颈在哪。如果瓶颈是计算量往数据并行上想如果瓶颈是不同工序的性质差异往任务并行上想。强行用错模型等于用螺丝刀拧螺母能拧但费劲。3. 内存模型与一致性并行程序最容易翻车的地方3.1 一个反直觉的例子先看一段伪代码这段代码能让所有并行新手栽跟头// 线程 A x 1; flag 1; // 线程 B while (flag 0) { } print(x);你觉得线程 B 会打印什么很多人会说1。但真实的计算机系统里它可能打印 0甚至可能永远卡在循环里出不来。为什么两个原因。第一编译器可能重排指令把flag 1提到x 1前面因为它看不出这两个变量有任何关系重排对单线程语义没影响。第二CPU 可能乱序执行即使编译后的汇编是对的现代处理器为了填满流水线也会调整指令的实际执行顺序或者在写缓冲里延迟写回导致线程 B 看到的顺序和线程 A 写的顺序不一致。这不是 bug这是计算机系统为了性能做的正常优化。但在并行场景下这些优化会破坏代码顺序就是执行顺序这个我们习以为常的假设。3.2 缓存一致性协议在做什么要理解为什么会出现上面的问题得先看多核处理器怎么管缓存。每个核心都有自己的私有缓存L1、L2共享最后一级缓存L3和主存。如果核心 A 把x 1写进自己的 L1核心 B 的 L1 里还留着x 0的旧副本两边就不一致了。硬件靠缓存一致性协议解决这个问题最经典的是 MESI 协议。MESI 用四个状态描述每一块缓存行的状态状态含义是否可直接读是否可直接写Modified本核已修改其他核没有是是Exclusive本核独占与主存一致是是Shared多核共享与主存一致是否需先升级Invalid数据已失效否否当一个核心想写某块数据时它必须先把其他核心的对应缓存行置为 Invalid这个动作叫获取独占权。这个过程需要跨核通信是有成本的。如果两个核心频繁读写同一块数据这块缓存行会在核心之间来回弹跳每次都要发消息协商——这就是著名的**伪共享False Sharing**的根源。3.3 伪共享看不见的性能杀手伪共享这个词听着玄乎用起来很坑。缓存行通常是 64 字节假设你有这样一个结构struct Counter { long a; long b; };两个线程分别频繁给a和b加一。从逻辑上看这俩变量毫无关系各加各的应该完全并行。但物理上a和b很可能在同一个 64 字节缓存行里。线程一改a会把整块缓存行置为独占线程二的副本失效线程二改b又要抢回来。两个核心像玩击鼓传花一样把同一块缓存行抢来抢去性能直接崩掉。解决办法很简单——填充Padding把两个变量隔开到不同的缓存行struct Counter { long a; char pad[56]; // 填充到 64 字节 long b; } __attribute__((aligned(64)));我实测过一个高频计数器场景未做填充时十六个线程的吞吐还不如单线程加了填充之后直接涨到七倍。这种坑不看代码根本发现不了因为你从逻辑上完全看不出它有问题。提示现代一些语言和库会自动处理伪共享。Java 的Contended注解、C 的std::hardware_destructive_interference_size都是干这个的。但大多数情况下你还是得自己意识到这个问题存在。3.4 内存屏障与原子操作回到 3.1 那个例子怎么让它正确答案是内存屏障和原子操作。内存屏障Memory Barrier / Fence是一条特殊的指令它告诉 CPU 和编译器屏障之前的操作必须全部完成后才能执行屏障之后的操作。它限制了重排保证了可见性。原子操作则是读-改-写三步不可分割的操作。以 CASCompare-And-Swap为例它的语义是如果内存里的值等于期望值就换成新值否则什么都不做并且整个过程是原子的。几乎所有无锁数据结构都建立在 CAS 之上。这里有个实操心得值得记住不了解内存模型就写无锁代码几乎必然写出错的程序。我见过太多人照着网上的模板写无锁队列测试时看着正常压力一上来就崩因为网上的模板往往省了必要的屏障或用了不正确的内存序。稳妥的做法是优先用语言提供的原子类型和现成的并发容器比如 C 的std::atomic明确指定内存序、Java 的java.util.concurrent包而不是自己从头造轮子。内存序本身也有一套体系从最弱的relaxed只保证原子性不保证顺序到最强的seq_cst全局顺序一致最安全也最慢理解它们的层级关系是进阶的关键relaxed只保证操作原子不约束重排。适合计数器这类不关心顺序的场景。acquire/release构成配对release 之前的写对 acquire 之后可见。最常见的发布数据模式。seq_cst所有线程看到的所有 seq_cst 操作顺序一致最直观但开销最大。用错内存序的后果是程序在 x86 上好好的换到 ARM 上就挂了——因为 x86 内存模型较强很多重排本来就不会发生而 ARM 允许更激进的重排。跨平台开发的场景下内存序必须严格对待不能心存侥幸。4. 并行编程的几种主流范式与选型思路4.1 共享内存范式共享内存是最直观的范式多个线程跑在同一块地址空间里直接读写公共变量。它的优点是数据共享方便一个线程算出来的中间结果另一个线程直接就能用不需要显式地拷贝数据。缺点是同步复杂你得时刻操心数据竞争、原子性、可见性这些问题写不对就是各种诡异的偶发 bug。线程和锁是最经典的组合。锁把并发访问变成了串行访问安全但可能成为性能瓶颈。我一般遵循的原则是锁保护的临界区尽量小只包住真正必须串行的部分尽量避免在持锁期间做 IO 或调用可能阻塞的函数优先用读写锁、无锁结构等更细粒度的方案但前提是你真的理解它们的语义共享内存适合数据交换频繁、负载相对均衡的场景比如图像处理、矩阵运算、内存数据库。4.2 消息传递范式消息传递的思路是每个进程有自己独立的内存空间想交换数据就发消息。MPI 是这个领域的代表。它的优点是不担心数据竞争因为根本不存在共享数据每个进程只管自己那块。扩展性特别好可以跨机器、跨节点。缺点是通信成本高每次交换都要显式地打包、发送、接收、解包延迟比共享内存里的内存访问高好几个数量级。消息传递适合大规模集群上的科学计算比如气象模拟、流体力学、分子动力学。这些场景数据量大、计算密集、通信相对稀疏能掩盖通信开销。4.3 数据流与流水线范式数据流范式把计算建模成数据在算子之间流动。每个算子是纯粹的变换输入进来输出出去。这种范式天生适合并行因为没有共享状态算子之间只通过数据交互。GPU 编程CUDA、OpenCL本质上是数据流的思想你描述一个 kernel它作用在整个数据集上硬件负责把它铺到成千上万个执行单元上。这种模式下程序员的关注点从怎么调度变成怎么把算法表达成规整的并行原语。流水线范式则强调连续数据处理各阶段同时工作。古老的按行处理、现代的流式处理框架都是这个思路。它的核心是把一个长任务拆成阶段让不同数据在不同阶段上同时流动就像工厂流水线。4.4 怎么选一张决策表面对具体场景用哪套范式我整理了一张决策表这个表是我自己在多年项目里摸索出来的实用为主场景特征推荐范式理由单机多核、数据共享频繁共享内存 线程池通信开销最低跨节点、计算密集消息传递MPI扩展性好不需要共享地址空间大规模规则数据运算SIMD/GPU 数据并行吞吐量天花板最高连续流式处理流水线各阶段天然重叠依赖关系复杂DAG 调度灵活表达依赖便于优化选错范式的代价非常大。我见过有人用共享内存 大量锁去做一个本质上是数据并行的任务结果锁竞争把性能彻底吃掉了改成 SIMD 之后快了二十倍。也见过有人用 MPI 去做单机上的交互式任务通信延迟让体验惨不忍睹换成线程就好了。选型的核心问题是通信/同步的频率和粒度。通信越稀疏越能容忍高延迟的方案MPI通信越密集越需要用低开销的共享内存加精细的同步设计。5. 加速比、扩展性与性能模型别被核数骗了5.1 Amdahl 定律串行部分的诅咒很多人以为八核就能加速八倍这个幻想被 Amdahl 定律无情击碎。Amdahl 定律说假设程序中有一部分必须串行执行占比为 s那么无论加多少核心加速比上限是1/s。举个例子。如果你的程序有 10% 必须串行那么极限加速比是 10 倍哪怕你有一千个核心也只能跑到十倍。如果串行部分占到 50%极限加速比就是 2 倍。这个定律的现实意义是优化并行之前先看看串行部分有多少。如果一个程序串行部分占 30%你花大力气优化并行部分基本上是白费正确的做法是先想办法把那 30% 并行化或者减少掉。实测中我经常看到这种情况一个任务单线程跑 100 秒其中 20 秒是数据加载和结果写回串行80 秒是可并行的计算。有人上来就把 80 秒的部分用了八个线程结果 20 10 30 秒加速比只有 3.3 倍他还纳闷为什么不是八倍。其实按 Amdahl 定律一算就明白了你优化错了地方。5.2 Gustafson 定律另一种视角Amdahl 定律假设问题规模固定。但实际中往往是问题规模随资源增长——有了更多核心你会想处理更大的数据。这时候该用 Gustafson 定律。Gustafson 定律说如果问题规模可以随核心数一起扩大那么加速比可以接近线性。因为随着问题变大可并行部分占比通常也变大串行部分占比相对缩小。这两个定律不矛盾它们回答的是不同问题Amdahl 回答同样的问题用更多核心能快多少Gustafson 回答更多核心能做多大的问题。理解它们的区别能帮你正确评估自己的程序到底是在哪个维度上优化。5.3 强扩展与弱扩展工程上更实用的两个概念是强扩展和弱扩展。强扩展Strong Scaling问题规模固定核心数增加看加速比。这是 Amdahl 场景加速比会逐渐饱和。弱扩展Weak Scaling每个核心处理的问题规模固定核心数增加问题规模也同比扩大看单核心效率能否保持。这是 Gustafson 场景理想情况下效率应该稳定在比较高的水平。我评估一个并行程序是否优秀通常先看弱扩展。如果弱扩展都做不到 70% 以上的效率说明通信或同步开销太大这个并行方案的架构大概率有问题。强扩展则是用来判断这个任务值不值得并行的如果强扩展在四核心就到头了那这台机器上再堆核心也没意义。5.4 怎么测加速比才靠谱测加速比这件事本身有很多讲究稍不注意就会测出误导性的数据必须多次运行取中位数或最小值单次的噪声太大。必须确保输入规模足够大小规模任务里启动和关闭线程的开销会占大头测出来的加速比毫无意义。必须预热尤其是涉及 JIT 编译或 GPU 的环境前几次运行都是热身。必须固定线程数环境别让操作系统和后台任务来捣乱。必须测完整的端到端时间而不是只测计算部分否则会被 Amdahl 定律偷袭。我踩过的最大的坑是测一个矩阵乘法规模设得不够大测出来十六线程比单线程还慢一度以为并行库有 bug后来把规模放大十倍加速比立刻变成接近十四倍。小规模并行测量的结果基本没有参考价值这是必须刻在脑子里的。6. 实际工程中的性能陷阱与调优清单6.1 陷阱一锁竞争锁竞争是共享内存并行的头号性能杀手。表现是线程数加了很多性能却不涨反降用分析工具一看大量时间花在等锁上。根因通常是临界区太大或者锁的粒度太粗。比如一个全局哈希表所有操作都抢一把大锁那不管多少线程都会排队。解决办法有几种思路按侵入性从小到大缩小临界区只把真正必须原子的部分包进来其他部分移出去。分片锁Lock Striping把一个大结构拆成若干分片每片一把锁不同 key 落到不同分片自然分散竞争。java.util.concurrent.ConcurrentHashMap就是这么做的。无锁化用 CAS 等原子操作替代锁。实现复杂收益视情况而定别盲目上。我个人的经验是先用分片锁解决大部分竞争问题无锁化只在 profiling 明确显示锁是瓶颈、且场景真的适合时才考虑。很多人一上来就想搞无锁结果既没快多少还引入了一堆难查的 bug。6.2 陷阱二负载不均理想情况下所有线程同时开工、同时完工。现实往往是有的线程十分钟干完在那闲着有的线程还在苦干总时间被最慢的线程拖住。根源是任务划分不均匀。比如按数据块切分时某些数据块的计算量就是比别的大规则切分反而制造了不均衡。解决办法任务窃取Work Stealing把任务切成很多小任务放进队列空闲线程从队列尾部偷活干。Java 的 ForkJoinPool、Intel TBB 都是这个思路。动态调度不预先划分而是让线程在运行时从共享任务池里领任务天然负载均衡。预测性划分如果能预知计算量差异可以按加权切分让每个线程分到的总计算量接近。任务窃取是目前最通用也最省心的方案代价是任务划分要足够细细到能填满调度粒度否则调度开销会吃掉收益。6.3 陷阱三过度并行化不是线程越多越好。每个线程都有自己的栈、自己的寄存器上下文创建、调度、切换都有开销。当任务太小时这些开销会超过并行带来的收益。一个常见现象一个 per-element 的操作每个元素的处理只要几纳秒你却给每个元素开一个任务。那绝大部分时间都花在任务调度上了。解决办法是批量化不要一个元素一个任务而是把元素攒成批次一批一个任务。批的大小要权衡一般让单个批次的计算时间在微秒到毫秒级别比较合适。6.4 陷阱四内存带宽瓶颈即使你的计算完美并行如果所有线程都在疯狂读内存那么内存带宽就成了公共瓶颈。多个核心抢内存总线实际性能远低于核心数。这不是算法问题是硬件限制。应对思路提高计算密集度让每个字节的读取对应更多的运算把带宽需求降下来。缓存分块Blocking把大数组切成能装进缓存的小块块内复用减少对主存的访问。共享数据多个线程如果读同样的数据尽量让它们读同一份缓存副本而不是各自从主存拉。矩阵乘法是典型的例子。朴素三重循环的内存访问模式很差分块之后能把主存访问降到很低性能提升十倍以上。这种优化本质上是把带宽受限变成计算受限。6.5 调试并行程序的实际手段并行 bug 难查因为它们往往是偶发的、时序相关的。我常用的手段有这么几个第一用死锁检测工具。大多数平台都有能自动识别锁顺序问题。第二强制改变调度和时序。加随机 sleep、故意打乱线程启动顺序让隐藏的竞争暴露出来。在一个设计不当的无锁队列上我靠这个方法五分钟复现了概率万分之一的 bug。第三用 sanitizer。数据竞争检测器如 ThreadSanitizer、地址检测器如 AddressSanitizer能自动发现大量并发问题。代价是运行时开销大但查 bug 时这点开销完全值得。第四日志加线程标记。所有日志都带上线程 ID 和时间戳事后分析时序关系。这招土但有效。第五简化复现条件。把线程数降下来、把数据规模降下来先在小规模上稳定复现再逐步扩大。很多 bug 在四线程上能复现在十六线程上才难查那就先在四线程上定位。7. 从零写一个并行程序完整思路拆解7.1 第一步判断可并行度拿到一个任务第一件事不是写代码是分析它能并行到什么程度。举个例子统计一个亿级数组里的素数个数。先看依赖关系。判断一个数是不是素数跟其他数的判断完全没有关系。这是完全可并行的任务理想情况下核数翻倍速度翻倍。再看计算量分布。素数的分布极不均匀小的数需要试除的因数少大的数需要试除的多而且素数本身比合数计算量大。所以不能简单地均分数组否则负载会严重不均。结论这是一个数据完美并行、但负载不均的任务。适合动态调度或者任务窃取。7.2 第二步选择粒度粒度太细调度开销大粒度太粗负载不均。素数统计这个任务的单个元素计算量是微秒级别如果每个元素一个任务调度开销会比计算本身还大。合适的做法是把数组分块块的大小使得单块计算时间在几十微秒到毫秒之间然后对这些块做任务窃取。这样既保证了负载均衡又摊薄了调度开销。7.3 第三步设计同步策略这个任务里各个线程计算的是不同区间的素数个数完全不需要同步。这是最理想的情况没有任何共享状态需要协调。只需要在最后把各线程的结果汇总一下。汇总这一步用原子加法或者最后的串行合并都行因为只执行一次开销可以忽略。如果一个任务需要频繁同步那就是设计上的红旗得重新考虑切分方式。我的一般原则是能避免同步就避免能降低同步频率就降低同步越少扩展性越好。7.4 第四步估算加速比上限按 Amdahl 定律算一下。假设数据的读取和结果汇总占总时间的 5%并行部分占 95%那么理论极限加速比是 1/0.05 20 倍。如果只用八核心理想加速比是 1 / (0.05 0.95/8) 1 / 0.169 5.9 倍。实测如果拿到 5 倍以上就算是很好的实现了。心里有这个数测出来的结果才知道是正常还是有问题。先算理论上限再看实测这个习惯能帮你快速判断问题出在哪。7.5 第五步实测与迭代写完了跑一遍看实际加速比和理论上限差多少。如果差得远逐个排查是不是负载不均用计时统计每个线程的耗时看方差。是不是共享状态有竞争检查有没有无意的共享变量比如各线程都写同一个累加变量。是不是内存带宽瓶颈看看是不是读数远超计算量。是不是粒度不对调大或调小块的大小试试。这个迭代过程通常要反复几次每次只改一个变量观察效果。并行调优就是这么一个试错过程没有一劳永逸的公式。8. 常见问题上手指南8.1 多少个线程最合适很多人问线程数设多少我的答案从来都是跟核心数挂钩通常设置为物理核心数或物理核心数附近。为什么不是逻辑核心数含超线程因为超线程共享物理核心的执行单元两个逻辑核对计算密集型任务的加速有限甚至因为争抢资源反而变慢。对于 IO 密集型任务线程数可以设得比核心数多因为线程大部分时间在等 IO。实测建议先设成物理核心数跑一遍再试物理核心数的 1.5 倍、2 倍看哪个最好。不同任务的表现差异很大没有万能值。8.2 什么时候不该并行不是所有任务都值得并行。判断标准任务本身太小总执行时间只有几百微秒并行的开销会把收益吃掉。串行占比太高按 Amdahl 定律串行超过 50% 就别折腾了。一次性任务就跑一次写并行的开发和调试时间远超省下的运行时间。需要强一致性的操作同步成本高到并行失去意义。我见过有人把只跑一次的脚本改成并行的结果调试花了三天省下的运行时间总共三分钟。这是典型的过度工程。8.3 并行程序的正确性怎么保证并行程序最大的挑战是正确性。几个实操建议第一所有共享数据要么用锁保护要么用原子操作没有中间地带。哪怕你觉得这个变量大概率不会被同时写也得老老实实保护起来因为时序问题永远比你想象的更刁钻。第二优先用经过验证的并发容器别自己造轮子。除非是学习和研究目的生产代码里的无锁结构都应该用现成的库。第三写测试用例时要考虑并发。单次运行正确不代表每次都正确需要重复运行、压力测试、用竞态检测工具。第四注意内存模型的平台差异。x86 上跑的代码在 ARM 上不一定对跨平台的项目要严格按规范写内存序。8.4 性能测量的常见误区前面提到的测量要多次、要预热、要足够规模这些是基础。还有几个更细的点别在调试模式下测性能优化开关没开测出来的结果毫无意义。别忽略系统噪声后台进程、电源管理、温度都会影响结果条件尽量控制一致。别只看平均值尾延迟比如 P99往往比平均值更重要尤其是对延迟敏感的场景。别用单一指标吞吐量和延迟是两个不同维度要一起看。9. 我自己踩过的几个坑以及给出的建议最后分享几个我在实际使用中印象深刻的教训都是文档里不会写、只有真正动手才会遇到的。第一个坑是关于预热。早期做 GPU 并行计算测出来第一版性能比 CPU 慢十倍差点就把方案换掉了。后来才发现GPU 的首次 kernel 加载和显存分配都很耗时前几次运行根本没进入稳定状态。加预热之后性能优势立刻显现出来。测并行性能预热不是可选项是必选项。第二个坑是关于测量粒度。有一次优化一个并行任务测到的加速比总是不理想。后来把计时代码细化发现时间根本不是花在计算上而是花在结果收集上——各个线程都往一个共享队列里塞结果队列的锁竞争成了瓶颈。改成每个线程先往本地缓冲写最后一次性合并性能立刻上去了。Profiling 要看分解别只看总量。第三个坑是关于内存序。写过一段无锁代码在本地 x86 机器上跑了几个月都没问题部署到 ARM 服务器上偶发崩溃。花了两天才定位到是一处内存序用得太弱本地的强内存模型掩盖了问题。跨平台场景下内存序一定要保守别图快用最弱的。第四个坑是关于负载均衡的假象。有个任务表面上数据是根据索引均匀分块的看起来应该很均衡。但实测加速比远低于预期分析之后发现数据的分布有偏前面的块计算量比后面小得多导致先做完的线程长期空闲。改成动态调度之后解决。别假设数据是均匀的要么分析分布要么用动态调度兜底。最后一个建议落到实处写并行程序之前先老老实实算出理论上限和串行部分占比。这个动作花不了十分钟能帮你避免大量无用功。如果理论加速比就在三五倍那你费大劲优化到七倍是不现实的早点接受这个事实把精力放在更值得的地方。并行计算是个需要敬畏的领域计算机系统给了你多个核心但能不能用好取决于你对同步、通信、内存模型、性能模型这些底层机制的理解深度。把这些东西吃透你会发现那些曾经诡异的性能问题和偶发 bug 都有清晰的解释和解决路径。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →