并行编程的目的

并行编程主要有三个目的:性能(performance),通用性(versatility),生产率(productivity)。简称PVP

注意区分并行和并发的概念:

并行:一个任务可以分割为多个子任务,其中的子任务可以在同一时刻执行。

比如计算1+2/3+4可以分割为:子任务一:1+4;子任务儿:2/3

并发:同时有多个相同的任务执行。比如:同时有多个用户访问https://github.com/
并行编程主要有三个目的:性能,生产率,通用性
性能(performance)

所有的并行编程都是为了提高程序的性能,在有限的硬件环境中,获取更高的性能。假如不是为了性能的提升,简单、快捷而安全的串行编程则是最佳选择。获取性能的提升的前提是现有程序性能很差,比如串行执行以下任务:

  1. 在数据库中(访问磁盘,IO操作)获取用户信息 。
  2. 调用第三方服务(网络通信,IO操作)获取学校信息。
  3. 判断该用户是否属于该学校学生 。

其中,第一步和第二步的执行顺序不分先后,若先执行第二步,也会获取到相同的结果。

若将第一步和第二步并行执行,也会获取到相同的结果。

假如,串行执行以上三步,整体的执行情况既可以获取到正确的结果,而整个过程耗时很短,则大可不必并行执行。只有在串行执行很慢(性能较差,这种情况一般会出现),则需要考虑优化性能,在优化过程中,首先需要优化步骤1、2(IO操作最耗时,优先优化)。其次,考虑并行步骤1、2。

通用性(versatility)

要想提高软件的开发成本,就是让软件具有通用性;比如:现在不会再有哪家公司会自己开发出一套数据,甚至连数据库的客户端,都不会开发。开源的许多软件具有的通用性,可以那里稍作修改与封装,便可使用。甚至可以直接使用

生产率(productivity)

在计算机硬件成本不断下降的同时,硬件的性能以及计算机的配置在不断提升,同时硬件的成本也在下降。

性能提升、成本下降的后果就是生产率的提升。仅是高效的利用性能,不足以体现软件的核心竞争力,高效的利用开发者,让开发者高效的利用性能。

PVP的权衡

能够提高生产率、性能、通用性都很高的软件是不存在的。如下图示:
并行:并行编程的基础概述
越接近底层,性能和通用性要求越高,比如JVM、MySQL、redis等等

越解决顶层,则需要有较高的生产率。因为顶层直接面对使用者,使用者千千万万,所以需要提高生产率,开发适用于不同的使用者的软件,比如淘宝网、GitHub、或者不同的服务等等

2 并行编程的两大定律

阿姆达尔定律(Amdaln Law) 软件编程中,【百度百科】对采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。阿姆达尔定律实际上定义了采取增强(加速)某部分功能处理的措施后可获得的性能改进或执行时间的加速比。简单来说是通过更快的处理器来获得加速是由慢的系统组件所限制。有如下公式:
S=1/(1a+a/n) S=1/(1-a+a/n)
S为加速比,即性能提升的倍数

a为并行计算部分所占比例

n为并行处理结点个数。

这样,当1-a=0时,(即没有串行,只有并行)最大加速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。

不同的n(并行节点数),s与a的变化关系如同所示:
并行:并行编程的基础概述

  1. 通过图可知,在a较小的时候(a<0.6)提升n,并不会带来性能的明显提升,当软件的并行部分较大的时候,提升n,可以获取到指数级别的增长。

    n一般是指cpu的个数,目前一台计算机的cpu最大8个,图中的cpu分别为2,5,25,50,100。对于多cpu的使用,可以通过计算机机器的形式,获取到超级cpu群。

  2. 古斯塔夫森定律(Gustafson Law)

    是对阿姆达尔定律的补充,格式:
    Sa(1a)n S=a+(1-a)n
    S为加速比,即性能提升的倍数

    a为串行计算部分所占比例

    n为并行处理结点个数。

    不同的n(并行节点数),s与a的变化关系如同所示:
    并行:并行编程的基础概述
    关于两个定律的思考:**

通过两个定律发现,在程序中,等串行部分较多时,提升并行处理的节点数,并未能显著提升程序的性能。在此情况下,(前提时程序的整体性能很低,只有在程序性能低的情况下,方可考虑优化,因为优化不是免费的)应该优先去优化串行执行部分,思考串行部分是否可以分割为并行执行,将串行执行的占比不断的降低,然后在考虑增加并行处理的节点数,这样方可提高整体的性能。

3 硬件结构

3.1 硬件体系结构

下图是一个粗略的8核计算机系统的硬件结构图
并行:并行编程的基础概述
数据以缓存行为单位,在内心,各个cpu缓存直接传输。当一个cpu从内存之中读取一个数据到该cpu的寄存器中的时候,必须要先将该数据读取的其缓存中(又叫高速缓存器)

3.2 cpu高速缓存

一次主内存的访问通常在几十到几百个时钟周期,一次L1高速缓存的读写只需要1~2个时钟周期一次L2高速缓存的读写也只需要数十个时钟周期,由于CPU运算速度要比内存读写速度快得多,所以会出现cpu高速缓存,来提高cpu的利用率。cpu高速缓存的容量小、访问速度快。

CPU缓存可分为:

  1. 一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
  2. 二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
  3. 三级缓存:简称L3 Cache,部分高端CPU才有

每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。

当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。

由于cpu高速缓存的存在,导致内存中的数据可能在cpu中拥有不同的值(多个不同的副本存在cpu高速缓存之中)

3.3 缓存一致性(MESI协议)

缓存一致性时管理缓存行的状态,以防止缓存行数据的状态不一致或者丢失数据,缓存行的数据状态有好多种,可以达到十种,常用的时四种状态,修改(modified)、独占(exclusive)、共享(shared)、无效(invalid)简称MESI协议。

M:处于修改状态的缓存行,是某个cpu对该内存数据的最近的一次操作,并且相应的内存中的数据没有在其他cpu的缓存行出现,此时,该缓存行要么将该数据写回内存,要么将该数据写到其他cpu的缓存中。

E:处于独占状态下的缓存行与修改状态下的缓存行很像,区别是被某个cpu独自享有,没有被其他cpu的缓存行修改,并且该cpu可以在任何时刻将数据存储到该缓存行,而不必考虑其他cpu。所以,该缓存行的数据在使用之后可以不写回内存或者其他cpu的缓存行,直接丢掉

S:处于共享状态的缓存行至少出现在一个cpu中,若被多个cpu共享,当其中任意一个cpu修改之后,必须要等待其他cpu同意,方可写回其缓存行。与独占状态相同,该状态下的数据可以直接丢掉,或者不写到其他cpu

I:处于失效状态的缓存行是空行,不持有任何有效数据,当新数据写入缓存行是,首要的状态就是失效状态,该状态的性能开销最小。

3.4 流水线CPU

20世纪80年代初的cpu在处理一条指令需要经历三个步骤:取指令,解码指令,执行指令,使用三个时钟完成,后来cpu可以像流水线那样执行指令,不过这个需要开发者可以给出高度可预测的控制流,这样cpu就可以满负载执行。当出现不可预测的控制流时,比如引用的不可达的数据,这是cpu的流水线就会被排空,等待重新加载指令

3.5 原子操作

程序执行之中不可分割的部分的执行,该部分之中的所有子执行,要么都成功,要么都失败

3.6 指令重排序

cpu操作的速度远远快于内存的操作速度,通过高速缓存可以解决这个倾斜的操作局面,但是当cpu高速缓存的数据来源与内存,当高速缓存中的数据失效之后,cpu将会出现缓存未命中,这时候cpu就要到内存之中重新读取该数据,但是这个读取的时间较长,这时,cpu会打乱原有程序(编程层)的顺序。执行后面的指令。前提时重排序之后不会影响程序执行的结果。

3.7 内存屏障

虽然重排序之后可以收获较好的性能,但是在有些程序块中,操作的成功,依赖内存操作的有序性,这时候就要禁止内存的重排序,出现了cpu中的一组指令,可以限制对内存中数据操作的顺序的重排序,即内存屏障。

相关文章: