优化策略之并行优化策略

并行优化策略顾名思义就是最大并行化的策略,那就需要我们合理最大化利用多核CPU、GPU以及集群。


分块:

  1. 任务级并行优化
  2. 数据并行优化
  3. 粒度调整

1.任务级并行优化

思想:

简单来说就是分治思想,大任务划分为独立的小任务,分配调度给核心或CPU来运行。

可以使用线程并行(OPENMP)、进程并行(MPI)以及GPU线程(CUDA)。

首先来解释一下多核CPU

多核cpu就是把两个及以上的处理核心集成到单一的物理处理器芯片的计算机处理器架构。这些核心共享同一块处理器资源(如缓存、内存控制器),但每个核心可以独立执行任务。与单核CPU相比,多核CPU可以在相同的时钟频率下同时执行更多任务,因此可以在一定程度上提高性能和响应速度。

  1. 处理核心:每一个核心都可以看作一个独立的处理器,都能够执行指令集。核心之间通过共享缓存高速互连网络进行通信。
  2. 缓存:多核CPU通常包括多级缓存结构,L1缓存通常是每个核心独享的,而L2和L3缓存则可以是共享或分级的。缓存减少了内存访问延迟,提高了数据处理速度。
  3. 内存控制器:管理核心与主内存之间的数据交换,通常集成在处理器芯片上。多个核心可以通过内存控制器并发访问内存。
  4. 互连网络:连接多个核心,使其能够共享数据和协调工作。不同的互连架构(如环形总线、Mesh网格)影响核心之间的通信效率和扩展性。

好的,这,我详细解释一下环形总线和mesh网络。

 在多核CPU中,随着核心数量的增加,核心之间的通信变得至关重要。为了提高性能和减少延迟,多核CPU设计了多种网络拓扑结构用于核心之间的数据传输。两种常见的网络拓扑结构是环形网络mesh网络,它们主要用来连接多核CPU的计算核心、缓存和其他组件。

  • 环形网络

环形网络是一种将所有核心和缓存通过一个单向或双向的环形总线连接起来的拓扑结构。每个节点(核心或缓存)通过一个接口与环路相连,数据通过环路在不同节点之间传递。常见于一些处理器设计中,尤其是具有较少核心数(如8-16核)的多核CPU。

单向环形网络:数据只能沿着一个方向(顺时针或逆时针)在环中传输。

双向环形网络:数据可以沿着两个方向传输,从而提高了带宽和降低了延迟。

优点:
  1. 简单实现:环形网络设计相对简单,易于实现和扩展。
  2. 低资源开销:与复杂的网络拓扑(如mesh网络)相比,环形网络对硬件资源的消耗较低。
  3. 适合小规模核心数量:在核心数量不多时(如10-20核),通信延迟可控,性能较好。
缺点:
  1. 延迟随核心数量增加:环形网络的一个主要问题是随着核心数量的增加,数据在环路中的传输时间变长,通信延迟增加明显。
  2. 带宽瓶颈:所有核心共享同一个环形总线,带宽有限,如果多个核心同时需要大量通信,会导致带宽拥塞。
  3. 单点故障风险:如果环形中的某个节点或通道出现故障,可能会影响整个系统的通信。
应用实例:
  • Intel的某些处理器(如Haswell、Broadwell等多核处理器)采用了环形总线来连接多个CPU核心和L3缓存。

                                                 

  • mesh网络(网格网络)

Mesh网络是一种二维网格(通常是2D)的网络拓扑结构,将多个CPU核心和缓存单元安排成行列矩阵的形式。每个节点(CPU核心或缓存单元)通过水平方向和竖直方向的连接,与它的邻近节点相连。数据通过这些网格连接从一个节点传递到另一个节点,直到到达目的地。

  • 二维mesh结构:每个核心与四个方向(上下左右)相连的其他核心通信,从而形成一个网格状结构。
  • 支持多跳通信:当两个不相邻的核心之间需要通信时,数据可以通过多个中间核心进行多跳传输。
优点:
  • 扩展性强:Mesh网络能够很好地扩展到大量核心。由于每个节点都可以与多个方向的邻居通信,网络带宽更大,适合更多核心数。
  • 较低的通信延迟:相比于环形网络,Mesh网络减少了核心之间的平均跳数,降低了延迟,特别是在大规模多核设计中效果显著。
  • 高带宽:由于多个核心之间可以并行通信,网格网络提供了较高的带宽。
  • 可靠性强:网格网络的节点和路径更多,出现单点故障时,网络可以通过其他路径绕过故障节点,不会影响整个系统的运行。
缺点:
  • 硬件开销大:由于每个节点需要连接多个其他节点,mesh网络的实现需要更多的布线和路由器,增加了硬件复杂性和能耗。
  • 复杂性较高:在大规模多核系统中,mesh网络的路由、流控等机制较为复杂,可能会增加设计和调试的难度。
应用实例:
  • Intel Xeon Phi系列处理器(如Knights Landing)采用了mesh网络架构,用于连接大量的CPU核心。
  • AMD的EPYC处理器和Intel的Skylake-SP、Cascade Lake等服务器级CPU,也采用了mesh网络来连接多个核心。

                                        

多核CPU的优化技术

 1.线程并行化

多核CPU允许通过多线程的方式并行化任务,以充分利用多个核心。

好的,这里解释一下为什么要多线程并行化任务来充分利用多个核心。

当一个任务(例如矩阵乘法、图像分块)进来时,我们通常会根据粒度将任务划分为粗粒度和细粒度。粗粒度的任务划分较大,适合在集群或超级计算机上执行;而细粒度任务划分较小,相关性强,适合多核CPU。

假设任务经过细粒度划分后,变成了多个子任务,这时操作系统的调度器会将进程或线程调度到不同的CPU核心上执行。那么我有一个问题:在单核CPU上,调度了一个进程及其线程到核心上执行的情况下,是不是这个核心只能通过时间片轮转来调度多个线程,这样就只能实现并发而无法实现真正的并行

首先,我的回答是在单核心处理器上,多线程并不是并行的,虽然看上去所有线程在并行执行,但实际上是通过时间片轮转的方式去调度的,只是说它的时间片比较短,操作系统会快速的切换不同线程,造成的一种错觉。

那么在多核心CPU上,多线程不仅有并发还存在并行,当一个进程被创建的时候,它至少包含一个主线程,也就是主线程在此核心上,其他线程可能会被调度到其他的核心上,这时候就是并行,但是并发的话,就是说我们这个核心上也可能除了主线程还存在其他线程进行时间片轮转,这时候就是并发。

so,多线程在多核cpu可以很好的利用多核心。

好的,这里在补充一下核心和进程与线程调度到问题。 

进程与线程的调度是由操作系统的调度器负责的,而不是核心直接调度进程,进程再调度线程。

进程调度:操作系统的调度器直接管理CPU核心的使用,并调度进程线程到CPU核心上执行。每个进程可以有一个或多个线程,这些线程可以由操作系统调度到不同的核心上运行。

线程调度:同样是操作系统调度线程。对于多线程应用,操作系统会将不同的线程分配给不同的核心运行,或者在同一核心上进行时间片轮转。因此,一个核心可以运行多个线程,具体由操作系统的调度算法决定。

这里再补充一下调度器与进程与线程的关系。 

  • 进程:是一个独立运行的程序实例,它包含了程序的代码、数据、打开的文件、分配的资源等。一个进程至少包含一个线程(即主线程),也可以包含多个线程。

  • 线程:是进程中的执行单元,线程在进程的地址空间内共享内存和资源,但每个线程有自己的栈和寄存器状态。多线程使得进程能够同时执行多个任务。

虽然进程可以创建和管理线程(例如通过操作系统的API),但进程并不负责线程的调度。

(也就是说进程相当于大厨,线程就是盘菜,这个菜呢可以由大厨炒出来(创建),大厨也可以调制咸淡(管理),但是大厨不能说把这个四川菜品移到陕西菜品中,这个移动(调度)问题就由调度器说了算的)

调度器是操作系统中的一个模块,负责管理系统中所有的进程和线程。调度器的主要职责包括: 

  • 调度进程和线程:调度器根据不同的调度策略(如优先级、时间片、负载均衡等)决定哪个线程或进程应该被分配到CPU核心上执行。
  • 分配CPU时间:调度器决定每个进程或线程在CPU核心上运行的时间片长短,特别是在单核系统上,多个线程或进程会通过时间片轮转执行。
  • 多核调度:在多核系统中,调度器决定如何将多个进程或线程分配到不同的核心上,最大化并行执行效率。

虽然进程不负责调度线程,但它可以创建管理销毁线程。一个进程可以通过系统调用(如pthread_createCreateThread等)生成新的线程,并控制它们的生命周期。进程对线程的控制包括:

  • 创建线程:进程可以生成多个线程,这些线程共享同一个进程的地址空间。
  • 线程同步:进程内的多个线程通过同步机制(如互斥锁、条件变量等)来协调它们之间的工作,防止资源争夺和数据竞争。
  • 终止线程:进程可以终止某个线程的执行,或者当进程终止时,所有线程也会随之终止。

操作系统的调度器会根据以下因素来决定如何调度进程和线程:

  • 优先级:不同的进程和线程可以有不同的优先级,优先级高的任务会优先获得CPU时间。
  • 多核CPU:调度器会将不同的线程分配到不同的CPU核心上,以实现并行执行。
  • 时间片轮转:在单核环境下,调度器通过时间片轮转的方式快速切换不同的线程或进程,使它们看似同时执行。

线程并行化是指将任务分解为多个线程,并将这些线程分配给不同的核心执行。可以使用以下技术来优化线程并行化:

OpenMP:用于共享内存系统的并行编程接口,能够简单高效地并行化循环和任务。

Pthreads:POSIX线程库,提供更底层的多线程控制,适用于需要精细控制的应用。

Intel Threading Building Blocks (TBB):为C++开发者提供了高级并行抽象,如任务调度和线程池管理。

好的,这里我们再补充一下任务与线程进程调度器整体的关系。

当一个任务进来的时候,首先我们需要根据粒度划分为子任务,这些子任务会由操作系统调度器来分配,首先操作系统调度器会分配一个进程,每个进程都有自己的虚拟地址空间,这意味着进程之间的内存是隔离的。进程可以包含一个或多个线程。这个进程可以视为一个小的独立空间,以供线程使用。

线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如执行栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。

重点!!!

当操作系统调度器分配任务时,它实际上是在调度进程。每个进程可以看作是一个大的任务单元,而线程则是这个大任务单元中的小任务单元。调度器通常会根据进程的优先级、资源需求、CPU时间片等因素来决定哪个进程应该获得CPU时间来执行。

在多线程环境中,线程是进程的一部分,它们共享进程的资源,但是操作系统调度器通常会对线程进行更细粒度的调度。这意味着在多线程的进程中,调度器可以决定哪个线程在某个时间点执行,从而实现更高效的资源利用和任务处理。

in a word,操作系统调度器分配的是进程,而线程是进程内部的任务执行单元,它们由进程内的线程调度机制来管理。在多线程程序中,操作系统的调度器负责进程级别的调度,而线程调度则通常由线程库或运行时环境来处理。

2.负载均衡

为了确保所有核心都能充分利用,必须进行负载均衡优化。这意味着在并行执行时,任务应均匀地分布在多个核心上,避免某些核心过载而其他核心空闲。

  • 静态负载均衡:在程序启动时就确定好任务的分配方式,适用于任务负载均匀且可预测的情况。
  • 动态负载均衡:在程序运行过程中动态调整任务分配,以应对任务负载的动态变化。 

3.缓存优化 

 多核CPU的性能也依赖于缓存的有效利用。缓存优化技术包括:

  • 数据局部性:优化程序的数据访问模式,以提高缓存命中率,减少内存访问延迟。(例如SPMV里面的CSR和COO等矩阵存储格式都是很好的数据局部性优化的例子)
  • 缓存对齐:确保数据结构在内存中的对齐方式最适合硬件架构,减少缓存冲突。

4.同步与互斥 

在多核CPU环境中,多个线程同时访问共享资源时,需要采取同步机制来避免数据竞争和保证数据一致性。然而,同步机制本身可能会引入性能开销,因为线程需要等待获取锁,这可能导致线程阻塞和上下文切换。为了减少这些开销,可以采用一些优化技术,其中细粒度锁和无锁编程是两种常用的方法。

细粒度锁

细粒度锁是一种减少锁竞争和等待时间的策略,它通过将锁的作用范围缩小到更小的数据单元来实现。这样可以减少锁的竞争程度,因为线程锁定的是较小的数据片段,而不是整个数据结构。具体来说:

  1. 减少锁的范围:而不是对整个数据结构加锁,可以将锁应用于数据结构的更小部分,比如单个元素或者数据结构的某个部分。

  2. 锁的粒度:锁的粒度越细,线程之间的竞争就越小,因为每个锁保护的数据越少,需要锁定的线程就越少。

  3. 减少锁持有时间:通过快速获取和释放锁,可以减少线程在锁上的等待时间,从而提高并发性能。

  4. 锁分离:在某些情况下,可以设计不同的锁来保护不同的数据,这样线程可以同时访问不同部分的数据而不会相互阻塞。

无锁编程

无锁编程是一种避免使用传统锁机制的并发编程方法,它依赖于原子操作和特殊的CPU指令来保证数据的一致性和线程之间的同步。无锁编程的目的是减少由于锁引起的性能开销。主要技术包括:

  1. 原子操作:原子操作是不可中断的单个操作,它们在执行过程中不会被其他线程中断。许多现代CPU提供了对原子操作的支持,如原子读写、原子增加和减少等。

  2. CAS指令:Compare-And-Swap(CAS)是一种常用的无锁编程技术,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。

  3. 避免锁的开销:无锁编程避免了传统锁机制带来的开销,如线程阻塞和上下文切换,因为线程不再需要等待获取锁。

  4. 顺序一致性:无锁编程通常需要确保操作的顺序一致性,以防止数据竞争和不一致的状态。

  5. ABA问题:CAS操作可能会遇到ABA问题,即在执行CAS操作期间,如果内存位置的值在某个时间点被更改为预期原值,然后又变回新值,CAS操作会错误地认为值没有变化。为了解决这个问题,通常使用版本号或者标记来跟踪值的变化。

 好的,这里我会详细解释一下锁这个概念

锁是同步机制中的一种基本工具,用于在多线程环境中协调对共享资源的访问。在计算机科学中,锁通常用于防止多个线程同时修改同一数据,从而避免数据竞争和不一致的状态。

锁的基本定义

锁是一种同步原语,它提供了一种方式来确保在任何时刻,只有一个线程可以访问特定的资源或代码段。锁通常有两种状态:锁定和解锁。

  • 锁定(Lock):当一个线程想要访问被锁保护的资源时,它首先需要获取锁。如果锁已经被其他线程持有,那么这个线程将等待直到锁被释放。
  • 解锁(Unlock):当线程完成对资源的访问后,它会释放锁,这样其他等待的线程就有机会获取锁并访问资源。

锁的类型

1. 互斥锁(Mutex):最常见的一种锁,确保多个线程不会同时访问同一资源。互斥锁在任何时候只允许一个线程持有。
2. 读写锁(Read-Write Lock):允许多个读操作同时进行,但写操作在执行时会阻塞所有其他读写操作。这适用于读操作远多于写操作的场景。
3. 自旋锁(Spinlock):一种简单的锁,当锁不可用时,线程会在一个循环中不断尝试获取锁,而不是进入等待状态。自旋锁适用于锁持有时间非常短的情况
4. 递归锁(Recursive Lock):允许同一个线程多次获取同一锁。这在递归函数中很有用,因为递归函数可能会多次尝试获取同一个锁。

细粒度锁

细粒度锁是相对于粗粒度锁的概念。在粗粒度锁中,锁的粒度较大,可能会锁定一大片代码或数据结构,导致只有少量线程能同时工作,即使这些线程访问的是不同的资源。这限制了并行度,因为很多线程可能在等待锁释放。细粒度锁则将锁的粒度减小,只锁定必要的资源或代码段。这样,更多的线程可以同时工作,因为它们访问的资源可能不重叠。细粒度锁可以减少线程之间的相互阻塞,提高并行度和性能。

锁的开销

使用锁是有开销的,包括:

  • 上下文切换:当一个线程等待锁时,它可能需要被挂起,让出CPU给其他线程。当锁被释放后,原来的线程可能需要被唤醒并重新获得CPU时间,这涉及到上下文切换的开销。
  • 锁竞争:如果多个线程频繁地请求同一个锁,它们可能会在锁上排队等待,增加了锁的竞争,降低了程序的整体性能。

锁的替代方法

由于锁可能引入性能瓶颈,因此在某些情况下,开发者可能会寻求锁的替代方法,比如:

  • 无锁编程:使用原子操作和避免共享状态来设计算法,从而避免使用锁。
  • 事务内存:一种高级的同步机制,它允许代码段像事务一样执行,要么完全成功,要么完全不影响系统状态。

锁是多线程编程中确保数据一致性和线程安全的重要工具,但它们需要谨慎使用,以避免引入不必要的性能开销。细粒度锁是一种减少这种开销的方法,它通过减少锁的范围来提高程序的并行度和性能。
 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值