06

并行处理器

从SISD到SIMD

阅读量:2 · 预计 16 分钟读完

多核GPUSIMD线程级并行
阅读进度2%

第六章 并行处理器

导读

并行处理是利用多个处理单元同时执行计算任务的技术,是突破单核性能瓶颈的关键途径。随着摩尔定律的放缓和功耗墙的限制,单核处理器性能提升越来越困难,并行处理成为提高计算性能的主要手段。从指令级并行到数据级并行,从线程级并行到请求级并行,并行技术已经渗透到计算机系统的各个层次。

本章将系统介绍并行处理的基本原理、并行架构的分类、多核处理器设计、GPU计算、以及并行编程模型。我们将从并行性的来源出发,深入分析各种并行技术的工作原理和优缺点,讨论并行编程的挑战和解决方案。通过学习本章,读者将理解并行处理的基本概念,掌握并行架构的设计原理,了解并行编程的方法。

核心概念详解

6.1 并行处理基础

6.1.1 并行性的来源

计算机系统中的并行性可以从多个层次挖掘。

指令级并行(ILP)

  • 多条指令同时执行
  • 通过流水线、超标量、乱序执行实现
  • 对程序员透明
  • 受限于数据依赖和控制依赖

数据级并行(DLP)

  • 对多个数据元素执行相同操作
  • 通过向量指令、SIMD、GPU实现
  • 需要编译器或程序员识别
  • 适合规则计算

线程级并行(TLP)

  • 多个线程同时执行
  • 通过多核、超线程实现
  • 需要操作系统支持
  • 适合并发应用

请求级并行

  • 同时处理多个请求
  • 在服务器和数据中心中常见
  • 通过多核、多线程实现
  • 适合服务负载

6.1.2 并行性能度量

加速比(Speedup)

加速比 = 串行执行时间 / 并行执行时间

效率(Efficiency)

效率 = 加速比 / 处理器数量

Amdahl定律

加速比 = 1 / ((1 - f) + f / p)

其中:

  • f:可并行部分的比例
  • p:处理器数量

Gustafson定律

加速比 = (1 - f) + f × p

考虑问题规模随处理器数量增加的情况。

CPI与并行

CPI_并行 = CPI_串行 / 并行度

6.1.3 并行编程模型

共享内存模型

  • 所有处理器共享同一地址空间
  • 通过读写共享变量通信
  • 需要同步原语(锁、信号量)
  • 编程相对简单

消息传递模型

  • 每个处理器有私有地址空间
  • 通过消息传递通信
  • 显式发送和接收
  • 可扩展性好

数据并行模型

  • 数据分布到多个处理器
  • 每个处理器执行相同操作
  • 隐式同步
  • 适合规则计算

6.2 多核处理器

6.2.1 多核架构

多核处理器在单个芯片上集成多个处理核心。

核心类型

同构多核

  • 所有核心相同
  • 负载均衡简单
  • 如Intel Core i7

异构多核

  • 核心类型不同
  • 大核(Performance)+ 小核(Efficiency)
  • 如ARM big.LITTLE、Intel Hybrid

核心配置

  • 2核、4核、8核、16核、32核、64核
  • 服务器处理器可达128核以上

6.2.2 片上互连

多核处理器需要高效的片上互连网络。

总线互连

  • 所有核心共享总线
  • 简单但带宽有限
  • 适用于少量核心

交叉开关(Crossbar)

  • 全互连
  • 高带宽
  • 面积和功耗大

片上网络(NoC)

  • 网格、环形、树形拓扑
  • 可扩展性好
  • 现代多核处理器常用

互连性能指标

  • 带宽:数据传输能力
  • 延迟:数据传输时间
  • 吞吐量:单位时间传输量

6.2.3 缓存一致性

多核处理器需要维护缓存一致性。

一致性问题

  • 每个核心有私有缓存
  • 共享数据可能有多个副本
  • 一个核心修改后其他副本失效

一致性协议

监听协议(Snoopy)

  • 所有缓存监听共享总线
  • 检测到写操作时更新或失效
  • 适用于总线互连

目录协议(Directory)

  • 使用目录记录缓存状态
  • 点对点通信
  • 适用于NoC互连

MESI协议

  • Modified:本地修改
  • Exclusive:独占一致
  • Shared:共享一致
  • Invalid:无效

6.2.4 同步机制

多核程序需要同步机制协调执行。

原子操作

  • 不可中断的操作
  • 如原子加、原子比较交换
  • 硬件支持

锁(Lock)

  • 互斥访问共享资源
  • 自旋锁、互斥锁
  • 需要避免死锁

信号量(Semaphore)

  • 计数器同步原语
  • P操作(等待)和V操作(信号)
  • 可以实现锁和条件变量

屏障(Barrier)

  • 等待所有线程到达
  • 用于并行区域的同步

事务内存(Transactional Memory)

  • 乐观并发控制
  • 事务要么全部执行,要么全部不执行
  • 简化并发编程

6.2.5 多核性能优化

负载均衡

  • 均匀分配工作到各核心
  • 避免部分核心空闲
  • 动态调度

数据局部性

  • 将数据分配到访问它的核心附近
  • 减少远程访问
  • NUMA感知调度

减少同步

  • 减少锁的使用
  • 使用无锁数据结构
  • 批量操作

核心休眠

  • 空闲核心进入低功耗状态
  • 提高能效比
  • 动态唤醒

6.3 向量处理器与SIMD

6.3.1 向量处理基本概念

向量处理器对一组数据(向量)执行相同操作。

向量指令

  • 单条指令处理多个数据
  • 如:向量加、向量乘
  • 提高数据级并行

向量寄存器

  • 宽度远大于标量寄存器
  • 128位、256位、512位
  • 容纳多个数据元素

向量长度

  • 向量中元素的数量
  • 固定长度或可变长度
  • 影响代码可移植性

6.3.2 SIMD指令集

x86 SIMD演进

MMX

  • 64位向量
  • 8个独立寄存器
  • 整数运算

SSE

  • 128位向量
  • 与浮点寄存器共享
  • 支持浮点运算

AVX

  • 256位向量
  • 独立的YMM寄存器
  • 3操作数指令

AVX-512

  • 512位向量
  • 独立的ZMM寄存器
  • 掩码操作
  • 散点/聚集

ARM SIMD

NEON

  • 128位向量
  • 与浮点寄存器共享
  • 丰富的数据类型支持

SVE

  • 可变长度(128-2048位)
  • 可扩展架构
  • 谓词寄存器

RISC-V向量扩展(RVV)

  • 可变长度向量
  • 灵活的配置
  • 开源标准

6.3.3 向量化编程

自动向量化

  • 编译器自动识别向量化机会
  • 生成向量指令
  • 需要代码适合向量化

手动向量化

  • 使用向量 intrinsic
  • 使用向量库
  • 更精确控制

向量化条件

  • 无数据依赖
  • 循环计数已知
  • 内存访问规则
  • 无复杂控制流

向量化示例

c
// 标量代码
for (i = 0; i < N; i++) {
    c[i] = a[i] + b[i];
}

// 向量化后
for (i = 0; i < N - VL; i += VL) {
    va = load_vector(&a[i]);
    vb = load_vector(&b[i]);
    vc = va + vb;
    store_vector(&c[i], vc);
}
// 处理剩余元素
for (; i < N; i++) {
    c[i] = a[i] + b[i];
}

6.3.4 向量性能分析

向量化收益

加速比 = 向量长度 × 向量效率

向量效率影响因素

  • 启动开销
  • 内存带宽
  • 数据依赖
  • 标量回退

性能瓶颈

  • 内存带宽限制
  • 向量寄存器压力
  • 标量代码比例

6.4 GPU架构与计算

6.4.1 GPU基本概念

GPU(Graphics Processing Unit)最初用于图形渲染,现已成为通用并行计算的重要平台。

GPU特点

  • 大规模并行:数千个处理核心
  • 高内存带宽:专用显存
  • 适合数据并行:SIMT执行模型
  • 高吞吐量:适合批量计算

GPU vs CPU

  • CPU:少量复杂核心,低延迟
  • GPU:大量简单核心,高吞吐量
  • CPU:复杂控制逻辑
  • GPU:简单控制,专注计算

6.4.2 GPU架构

流式多处理器(SM)

  • GPU的基本计算单元
  • 包含多个CUDA核心
  • 共享内存和寄存器

CUDA核心

  • 基本处理单元
  • 执行浮点和整数运算
  • 简单的控制逻辑

内存层次

全局内存

  • 显存(GDDR、HBM)
  • 高带宽
  • 高延迟
  • 所有线程可见

共享内存

  • 每个SM私有
  • 低延迟
  • 线程块内共享
  • 软件管理

寄存器

  • 每个核心私有
  • 最快访问
  • 数量有限

常量缓存和纹理缓存

  • 只读数据缓存
  • 优化特定访问模式

6.4.3 GPU编程模型

CUDA编程模型

  • 主机(CPU)和设备(GPU)
  • 内核函数在GPU执行
  • 线程层次:网格→块→线程

线程组织

  • 线程(Thread):基本执行单元
  • 线程块(Block):线程组,共享内存
  • 网格(Grid):线程块组

SIMT执行

  • 单指令多线程
  • 线程束(Warp):32个线程
  • 线程束内线程同步执行
  • 分支发散降低效率

内存管理

  • 主机和设备内存分离
  • 显式数据传输
  • 统一内存(Unified Memory)

CUDA编程示例

c
// 内核函数
__global__ void vectorAdd(float *c, float *a, float *b, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        c[i] = a[i] + b[i];
    }
}

// 主机代码
int main() {
    // 分配设备内存
    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, size);
    cudaMalloc(&d_b, size);
    cudaMalloc(&d_c, size);
    
    // 复制数据到设备
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
    
    // 启动内核
    int threads = 256;
    int blocks = (n + threads - 1) / threads;
    vectorAdd<<<blocks, threads>>>(d_c, d_a, d_b, n);
    
    // 复制结果回主机
    cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);
    
    // 释放设备内存
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    
    return 0;
}

6.4.4 GPU性能优化

内存合并访问

  • 线程束内线程访问连续内存
  • 合并为一次内存事务
  • 提高内存带宽利用率

共享内存使用

  • 减少全局内存访问
  • 线程块内数据共享
  • 需要处理bank冲突

占用率优化

  • 活跃线程束与最大线程束的比值
  • 受寄存器和共享内存限制
  • 平衡资源使用

分支优化

  • 减少线程束内分支发散
  • 线程束内所有线程执行所有路径
  • 降低执行效率

流水线优化

  • 重叠计算和内存访问
  • 使用多个线程块隐藏延迟
  • 异步内存复制

6.5 并行编程模型

6.5.1 共享内存编程

OpenMP

  • 编译器指令
  • 适用于循环并行化
  • 支持C/C++/Fortran

OpenMP示例

c
#pragma omp parallel for
for (int i = 0; i < N; i++) {
    c[i] = a[i] + b[i];
}

Pthreads

  • POSIX线程标准
  • 显式线程创建和管理
  • 底层控制

Pthreads示例

c
void *thread_func(void *arg) {
    // 线程执行代码
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL);
    pthread_join(thread, NULL);
    return 0;
}

6.5.2 消息传递编程

MPI(Message Passing Interface)

  • 分布式内存编程标准
  • 进程间消息传递
  • 支持C/C++/Fortran

MPI示例

c
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);

// 发送数据
MPI_Send(data, count, MPI_FLOAT, dest, tag, MPI_COMM_WORLD);

// 接收数据
MPI_Recv(data, count, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &status);

// 集合操作
MPI_Bcast(data, count, MPI_FLOAT, root, MPI_COMM_WORLD);
MPI_Reduce(send_data, recv_data, count, MPI_FLOAT, MPI_SUM, root, MPI_COMM_WORLD);

MPI_Finalize();

6.5.3 数据并行编程

MapReduce

  • 大规模数据处理框架
  • Map阶段:数据映射
  • Reduce阶段:数据归约
  • 自动并行化和容错

数据流编程

  • 数据驱动执行
  • 无共享状态
  • 自动并行

6.5.4 异构编程

CUDA + OpenMP

  • CPU使用OpenMP并行
  • GPU使用CUDA加速
  • 协同工作

OpenCL

  • 开放标准
  • 支持多种设备(CPU、GPU、FPGA)
  • 可移植性好

SYCL

  • 基于C++的高层抽象
  • 单源编程
  • 类型安全

6.6 并行性能分析

6.6.1 性能瓶颈识别

Amdahl定律限制

  • 串行部分限制加速比
  • 需要最大化并行部分
  • 识别和减少串行代码

通信开销

  • 进程间通信延迟
  • 同步开销
  • 数据一致性维护

负载不平衡

  • 部分处理器空闲
  • 动态负载分配
  • 工作窃取

资源竞争

  • 内存带宽竞争
  • 缓存一致性流量
  • 互连网络拥塞

6.6.2 性能分析工具

并行性能工具

  • Intel VTune:性能分析
  • NVIDIA Nsight:GPU分析
  • TAU:多平台分析
  • Score-P:性能分析

分析指标

  • 加速比和效率
  • 负载平衡
  • 通信开销
  • 同步开销
  • 缓存行为

6.6.3 可扩展性分析

弱可扩展性

  • 问题规模随处理器数量增加
  • 保持每个处理器的工作量不变

强可扩展性

  • 问题规模固定
  • 增加处理器减少执行时间

等效率函数

  • 保持效率不变时,问题规模与处理器数量的关系

重要知识点

知识点1:Amdahl定律

Amdahl定律描述了并行加速的理论极限。

公式

加速比 = 1 / ((1 - f) + f / p)

其中:

  • f:可并行部分比例
  • p:处理器数量

含义

  • 即使并行部分无限加速,整体加速比受限于(1 - f)
  • 例如:10%串行代码,最大加速比为10

启示

  • 减少串行部分很重要
  • 无限增加处理器收益递减
  • 需要平衡并行度和开销

知识点2:Gustafson定律

Gustafson定律考虑问题规模随处理器数量增加的情况。

公式

加速比 = (1 - f) + f × p

与Amdahl定律的区别

  • Amdahl:固定问题规模
  • Gustafson:问题规模随处理器增加

实际意义

  • 大规模问题可以更好地并行化
  • 并行计算的实际加速比可能更高

知识点3:GPU线程层次

GPU的线程组织层次:

线程(Thread)

  • 基本执行单元
  • 有自己的寄存器和程序计数器

线程块(Block)

  • 线程组
  • 可以共享内存
  • 可以同步

网格(Grid)

  • 线程块组
  • 执行同一内核

线程束(Warp)

  • 32个线程
  • SIMT执行
  • 基本调度单位

知识点4:并行效率

并行效率衡量并行化的效果。

公式

效率 = 加速比 / 处理器数量

理想效率

  • 效率 = 1:完美线性加速
  • 效率 < 1:存在开销
  • 效率 > 1:超线性加速(罕见)

影响因素

  • 通信开销
  • 同步开销
  • 负载不平衡
  • 串行部分

知识点5:NUMA架构

NUMA(Non-Uniform Memory Access)架构:

特点

  • 多个内存节点
  • 访问本地内存快
  • 访问远程内存慢

编程考虑

  • 数据局部性重要
  • NUMA感知的内存分配
  • 线程绑定到核心

优化策略

  • 第一接触策略
  • 内存交织
  • 显式NUMA控制

常见误区

误区1:核心数越多性能越好

核心数增加并不总是带来线性性能提升。

限制因素

  • Amdahl定律限制
  • 通信和同步开销
  • 内存带宽限制
  • 功耗限制

实际情况

  • 并行度高的应用受益明显
  • 串行部分多的应用受益有限
  • 需要考虑软件并行化程度

误区2:GPU总是比CPU快

GPU的性能优势取决于应用特性。

GPU优势场景

  • 大规模数据并行
  • 规则计算
  • 高算术强度

CPU优势场景

  • 复杂控制流
  • 低并行度
  • 低延迟要求

正确选择

  • 根据应用特性选择
  • 异构计算结合两者优势

误区3:并行编程很简单

并行编程面临诸多挑战。

挑战

  • 数据竞争
  • 死锁
  • 负载不平衡
  • 调试困难

正确态度

  • 需要专门的并行思维
  • 使用成熟的编程模型
  • 充分测试和验证

误区4:向量化总是能提高性能

向量化并不总是带来性能提升。

限制因素

  • 数据依赖
  • 内存带宽限制
  • 标量化开销
  • 代码复杂度

适用场景

  • 数据并行
  • 规则访问模式
  • 高算术强度

误区5:并行程序自动可扩展

并行程序的可扩展性需要精心设计。

问题

  • 通信开销随规模增加
  • 负载不平衡加剧
  • 资源竞争增加

解决方案

  • 算法可扩展性设计
  • 减少通信
  • 动态负载均衡

实践应用

应用1:科学计算并行

科学计算是并行计算的主要应用领域。

应用领域

  • 气候模拟
  • 分子动力学
  • 流体力学
  • 天体物理

并行策略

  • 域分解
  • 粒子分解
  • 混合并行(MPI + OpenMP)

性能优化

  • 减少通信
  • 重叠计算和通信
  • 使用专用库

应用2:机器学习并行

机器学习广泛使用并行计算。

数据并行

  • 数据分布到多个设备
  • 每个设备计算梯度
  • 聚合梯度更新模型

模型并行

  • 模型分布到多个设备
  • 适合大模型
  • 需要流水线并行

框架支持

  • TensorFlow
  • PyTorch
  • Horovod

应用3:数据库并行

数据库系统使用并行提高性能。

查询并行

  • 并行扫描
  • 并行连接
  • 并行聚合

事务并行

  • 多版本并发控制(MVCC)
  • 乐观并发控制
  • 分布式事务

分布式数据库

  • 数据分片
  • 复制
  • 一致性协议

应用4:高性能计算集群

HPC集群是并行计算的主要平台。

集群架构

  • 计算节点
  • 互连网络(InfiniBand)
  • 存储系统

编程模型

  • MPI
  • OpenMP
  • CUDA

资源管理

  • 作业调度(Slurm)
  • 资源分配
  • 监控和管理

本章小结

本章系统介绍了并行处理器的设计原理和技术,主要内容包括:

并行处理基础:介绍了并行性的来源(ILP、DLP、TLP)、并行性能度量(加速比、效率、Amdahl定律)和并行编程模型(共享内存、消息传递、数据并行)。

多核处理器:详细讲解了多核架构、片上互连、缓存一致性、同步机制和性能优化。多核处理器是现代计算的主流架构。

向量处理器与SIMD:讨论了向量处理的基本概念、SIMD指令集(x86 SSE/AVX、ARM NEON/SVE、RISC-V RVV)、向量化编程和性能分析。向量处理是提高数据级并行的重要手段。

GPU架构与计算:深入介绍了GPU架构、编程模型(CUDA)、性能优化技术。GPU已成为通用并行计算的重要平台。

并行编程模型:介绍了OpenMP、Pthreads、MPI、MapReduce等编程模型,以及异构编程技术。选择合适的编程模型对并行程序的成功至关重要。

并行性能分析:讨论了性能瓶颈识别、分析工具和可扩展性分析。性能分析是优化并行程序的关键。

并行处理是突破单核性能瓶颈的主要途径。从多核处理器到GPU,从共享内存到分布式系统,并行技术已经渗透到计算机系统的各个层次。未来的并行计算将面临异构计算、大规模并行、能效优化等挑战,需要新的架构创新和编程模型来应对。

下一章将讨论存储系统,这是计算机系统中另一个重要的组成部分。