并行程序设计-第三讲.SIMD编程


SIMD编程

SIMD概念

image-20221024141506247

SPMD单个程序在不同数据流上执行

本讲主要介绍单核向量编程

SIMD编程概述

  • 向量计算机

  • 早期的SIMD超级计算机:银河

  • 当前的SIMD架构

    • 多媒体扩展:SSE、AVX
    • 图形和游戏处理器:CUDA
    • 协处理器:Xeon Phi
  • 没有占压倒优势的SIMD编程模型

    • 向量计算机都是科学家用来编程
    • 多媒体扩展指令集多是系统程序员在用
    • GPU多是游戏开发者、大数据分析人员使用
  • 标量和SIMD(多媒体扩展架构)差别

    • image-20221024142859008
  • 多媒体扩展架构的核心

    • SIMD并行

    • 可变大小的数据域

    • 向量长度=寄存器宽度➗类型大小

    • image-20221024143637806

      这里有128位寄存器,存储数据的大小由数据类型决定,比如如果存储长整型(32字节)的话,只能支持4个数同时计算

  • 适合应用SIMD的特点

    • 规律的数据访问模式
      • 数据项在内存中连续存储
    • 短数据类型:8、16、32位
    • 流式数据处理,一系列处理阶段
      • 时间局域性,数据流重用
    • 很多情况下可用来提升计算效率
      • 很多常量
      • 循环迭代短
  • 为什么采用SIMD

    • 更大的并发度
    • 设计简单、重复功能单元即可
    • 更小的芯片设计
    • 缺点:代码很底层繁琐
  • 多媒体扩展编程

    • 语言/指令集扩展

      • 程序接口类似函数调用

      • C/C++:内置函数、 intrinsics

      • 大多数编译器支持多媒体扩展

        ➢gcc:-march=corei7, -faltivec

        SSE2: dst= _mm_add_ps(src1, src2);

        AltiVec: dst= vec_add(src1, src2);

        Neon: dst = vaddq_f32(src1, src2)

        ➢无统一标准

      • 很多编译器支持自动编译

SIMD并行(不是很重要,了解即可)

几个例子

  • image-20221024145431584

    把重复的算术运算变成向量运算

  • image-20221024150231461

    存取数据的次数降低,效率提升

  • 可向量化的循环

    image-20221024150338675 image-20221024150422081
  • 可部分向量化的循环(有数据依赖)

    image-20221024150628063 image-20221024150649061

SIMD编程的额外开销

打包/解包数据的开销:重排数据使之连续

  • image-20221024151459299
  • 打包源运算对象——拷贝到连续内存区域
    • image-20221024151536575
  • 解包目的运算对象——拷贝回内存
    • image-20221024151628154

对齐:调整数据访问,使之对齐

  • 对齐的内存访问

    • 地址总是向量长度的倍数(例如16字节)
    • image-20221024152331590
  • 未对齐的内存访问

    • 地址不是16字节的整数倍

    • 静态对齐:对未对齐的读操作,做两次相邻的对齐读操作,然后进行合并

    • 有未对齐相应操作函数,仍会产生多次内存操作

    • 底层硬件的操作:image-20221024152556088

    • 静态调整循环

      image-20221024153232518
    • 动态对齐:

      不知道从几开始,虽然有额外的对齐开销,但是结果一定是正确的

      image-20221024153357734

  • 小结

    • 最坏情况需要计算地址,动态对齐

    • 编译器/程序员可分析确认对齐

      • 一般而言数据是从起始地址处对齐
      • 如果在一个循环中顺序访问数据,起始位置固定,则对齐特性是不变的
    • 可调整算法,先串行处理到对齐边界, 然后进行SIMD计算 

    • 有时对齐开销会完全抵消SIMD的并行收 益

控制流可能要求执行所有路径

  • image-20221025162120878

  • 底层实现

    image-20221025162249109
  • 能否改进

    • 假定所有控制流路径执行频率都不同
    • 应该针对频率最高的路径优化代码
    • 其他路经按默认方式执行
    • image-20221025162455276
  • 控制流开销小结

    image-20221025162609859

SIMD编程复杂性

image-20221025162755998

SSE/AVX编程

X86架构

发展历史

  • X86:Intel开发的一种微处理器体系结构

  • 出现:1978年Intel 8086 CPU中

    • 但是一般来说X86指的是X86_32bit,32位系统
    • 64位就是指X86_64bit。简写为X64
  • 发展:

    • ❑ 1971-1992年数字编号:80X86系列

    • ❑ 1993-2005年奔腾系列:Pentium

    • ❑ 2005酷睿系列:Core

基本框架

image-20221025163551311
  • 基本的执行模式 
  • 数据类型 
  • 指令集合(只讲这个) 
    • ❑ 通用指令(传送,算术,逻辑,控制等)
    • ❑ X87 FPU指令(传送,算术,比较,控制等 )
    • ❑ MMX指令(传送,转化,打包,比较等)
    • ❑ SSE指令(增加寄存器,SIMD浮点数运算)
    • ❑ SSE2指令(整数指令,64-bit SIMD浮点运 算)
  • 寄存器

x86架构SIMD支持

image-20221025164145509

SSE指令集

是什么

image-20221025164554037

发展历史

  • SSE是MMX的超集

  • MMX

    • ❑ 1996年Intel在奔腾处理器集成MMX指令, 为应对音频、图片、视频等多媒体应用的密 集的计算需求
    • 64-bit的MMX寄存器(8个,复用了浮点寄存器的尾部,与x87共用寄存器,缺少浮点指令)
    • ❑ 支持在打包的字,字节,双字整数上的 SIMD操作
  • SSE128bit寄存器与MMX寄存器的区别

    • image-20221025165011232
    • AVX 256位

AVX指令集

image-20221025165156622

SSE编程

image-20221025165341462

SSE指令

数据移动指令

将数据移入/出向量寄存器

image-20221025165835481
算术指令

多个数据(2 doubles、4 floats等) 上的算术运算

  • ❑ PD:两个双精度,PS:四个单精度,SS:标量
  • ❑ ADD、SUB、MUL、DIV、SQRT、MAX、MIN、RCP等
    • ➢ADDPS:四个单精度加法;ADDSS:标量加法
逻辑指令

多个数据上的逻辑运算

  • ❑ AND、OR、XOR、ANDN等
    • ➢ANDPS – 运算对象位与
    • ➢ANDNPS – 运算对象位与非
比较指令

多个数据上的比较运算

❑ CMPPS、CMPSS:比较运算对象,每个比较结果影 响SIMD寄存器中32位——全1或全0

洗牌指令

在SIMD寄存器内移动数据

  • ❑ SHUFPS:从一个运算对象洗牌数据保存到另一个运 算对象
  • ❑ UNPCKHPS:解包高位数据到一个SIMD寄存器
    • image-20221025170117239
  • ❑ UNPCKLPS
其他指令
  • ❑ 类型转换:CVTPS2PI mm,xmm/mem64
  • ❑ 缓存控制
    • ➢MOVNTPS将浮点数据从一个SIMD寄存器保存到内存,绕 过缓存
  • ❑ 状态管理:LDMXCSR读取MXCSR状态寄存器

SSE C/C++编程

  • SSE指令对应C/C++ intrinsic

    • ❑ intrinsic:编译器能识别的函数,直接映射 为一个或多个汇编语言指令。Intrinsic函数 本质上比调用函数更高效
    • ❑ Intrinsics为处理器专有扩展特性提供了一个 C/C++编程接口
    • ❑ 主流编译器都支持,如GCC
  • 使用SSE intrinsics所需的头文件(向前兼容

    • image-20221025170622370
    • 向前兼容:比如我想要使用SSE3,其中就包含了SSE和SSE2
  • 编译选项: -march=corei7

  • AMD CPU对MMX/SSE/SSE2支持较好,SSE4支持较差

矩阵乘法

串行版本
void mul(int n,float a[][maxN],float b[][maxN],float c[][maxN]){
  for (int i=0;i<n;i++){
    for(int j=0;j<n;j++){
      c[i][j]=0.0;
      for(int k=0;k<n;k++){
        c[i][j]=a[i][k]*b[k][j];
      }
    }
  }
}
cache优化

把ab两个矩阵都变成行主序

void trans_mul(int n,float a[][maxN],float b[][maxN],float [][maxN]){
  //转置
  for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
      swap(b[i][j],b[j][i])
      
	for (int i=0;i<n;i++){
    for(int j=0;j<n;j++){
      c[i][j]=0.0;
      for(int k=0;k<n;k++){
        c[i][j]=a[i][k]*b[j][k];//⚠️是b[j][k],不是b[k][j]
      }
    }
  }
  
  //转置
  for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
      swap(b[i][j],b[j][i])
}
SSE版本🌟

不懂

void sse_mul(int n,float a[][maxN],b[][maxN],float c[][maxN]){
  __m128t1,t2,sum;
  //转置
  for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
      swap(b[i][j],b[j][i]);
      
  for(int i=0;i<n;i++){
    for(int j=0;j<n;j++){
      c[i][j]=0;
      sum=_mm_setzero_ps();
      for(int k=n-4;k>=0;k-=4){
        t1=_mm_loadu_ps(a[i]+k);
        t2=_mm_loadu_ps(b[j]+k);
        t1=_mm_mul_ps(t1,t2);
        sum=_mm_add_ps(sum,t1);
      }
      sum=_mm_hadd_ps(sum,sum);
      sum=_mm_hadd_ps(sum,sum);
      _mm_store_ss(c[i]+j,sum);
      //如果还有不能被4整除的部分
      for(int k=(n%4)-1;k>=0;k--){
        c[i][j]+=a[i][k]*b[j][k];
      }
    }
  }    
    //转置
  for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
      swap(b[i][j],b[j][i]);
}
分片策略

啥玩意啊

看不懂

image-20221101214350768

image-20221101214331724


文章作者: zxn
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zxn !
  目录