前言
Rust 是一门享誉中外的安全和高效的系统编程语言,业界各大平台包括华为选择和引入 Rust 这门语言作为自己的产品的开发语言。
华为在图片的识别算法中验证了这门语言的安全和高效性。并使用高级计算加速技术使其达到了效果倍增的效果,并超过了使用 C 语言实现得到的最好效果,而 SIMD 技术(单指令流多数据流)发挥了最重要的作用。
本文以图片脏污检测算法优化案例为基础,主要介绍 Rust 中一些以 SIMD 技术为主的计算加速类应用,希望对大家在今后的学和开发过程中有所帮助。
Rust 语言计算加速效果图
首先来看,使用 Rust 语言的计算加速优化效果,下图产品检测中的赃污检测算法在优化前后的性能对比:
当我们分别在 x86_64 以及 aarch64 两种主流 CPU 架构上进行 Rust 优化前后的性能效率对比。
在 x86_64 CPU 架构中我们优化前的单帧耗时为 1.585 ms,而优化后的耗时减少一倍到 0.65 ms。同理在 aarch64 架构的 CPU 优化前是 2.62 ms,优化后也减少到 1.25 ms,有着相同明显的效率提升。
其次,我们可以看到使用 Rust 和 C 的分别能获得的优化效果对比:
Rust 语言相比于 C 语言来说,能获得的最高效率还要快 25% 左右。当然,这并不代表 Rust 语言在效率上天生有优势。而是在计算加速的使用上 Rust 程序做的更好。
什么是 SIMD 技术
SIMD,英文全称 Single Instruction Multiple Data ,中文翻译为单指令多数据流。一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
SIMD 是一种基于特殊 CPU 计算单元的性能优化技术。顾名思义,指的是在一条 CPU 指令执行期间可以执行多条数据的计算。主要运用于科学计算、多媒体处理等数据密集型运算场景下。
借助于此种方法,一般可达到数倍甚至数十倍的性能提升。
比如左图,对于连续的加法运算,传统的实现是将数据依次进行每对操作数的加法指令。而 SIMD 技术会依次读取多个数据,组成一对 SIMD 加法向量,放入特殊的向量计算器中,然后使用专门的 CPU 指令计算出这对向量的和。
这个特殊的 CPU 指令主要靠下图中的 SIMD 处理单元结构来实现的。
目前主流的 CPU 架构比如 X86、ARM、MIPS 都集成了成千上百个这样的 SIMD 处理单元,每一个都对应了一种或多种 SIMD 运算。
总的来说,SIMD 技术是一种软件中充分调用硬件性能,实现性能倍增的软硬件协同技术。
SIMD 技术业界优秀实践
这里列出了 SIMD 比较出名的业界优秀软件实践。
- Numpy:多维数组计算开发库,主流用 Python 语言做数字计算开发,它的实现中有许多地方做了 SIMD 开发优化。
- OpenCV:计算机视觉库,它的核心 Universal Intrinsics API 基本都是基于 SIMD 来实现的。
- IPP:Intel® IPP,因特尔的多媒体计算库,里面的函数实现都使用了 SIMD 指令进行优化。
- OpenBLAS:一个开源的矩阵计算库,包含了诸多的精度和形式的矩阵计算算法。
- KML:华为自研的数学库,鲲鹏数学库(Kunpeng Math Library,以下简称 KML)是基于华为鲲鹏处理器的高性能数学计算加速库,提供了基于鲲鹏平台优化的数学函数。
可以说,只要是以计算性能为核心竞争力的软件,SIMD 都是不可绕开的核心技术。
Rust 语言中的 SIMD
SIMD 在 Rust 语言社区中是以 RFC 被提出,经过一两年的讨论,作为 Roadmap 路径之一进行主要的开发工作,涉及从业务代码贯穿到底层硬件架构,是一个跨度大、工作量也很大的一个特性。
架构设计
架构图设计图如下:
底层硬件架构
底层的硬件架构,每种 CPU 架构类型提供的 SIMD 指令都是该架构专用的,这就导致了同样的运算会对应不同的 SIMD 指令。
而且每种 CPU 架构都会随着其硬件版本的扩展而随之扩展。与之产生了 SIMD 指令集这个概念。比如 x86 上的 AVX、SSE;arm 上的 neon 和 SVE 指令集等等。
LLVM
在硬件之上做了一层抽象,集成支持了各种主流的 SIMD 指令集的汇编生成。而语言编译器可以使用 LLVM 产生 SIMD 想要的汇编指令,而不用自己嵌入汇编代码,因为 Rust 语言天生就以 LLVM 作为主流后端,所以说在 Rust 中支持 SIMD 具有天然的优势,这也是 Rust-SIMD 技术的实现基础。
计算加速库、多平台适用层、业务代码
Rust SIMD 的绝大部分开发工作是在 Rust 编译器、计算加速库 stdarch、多平台适配层这三层;计算加速库 stdarch 以用户接口的方式集成了各种 SIMD 指令集,由于 Rust 编译器而编译成相应的 LLVM IR,并传递给 LLVM,由此间接的生成需要的 SIMD 汇编指令。而计算加速库 stdarch 之上还有一层多平台适配层,因为我们之前提到的 SIMD 的加速指令是各加各或各平台专用的,并且相同架构不同指令集所使用的数据长度也不一样,不便于用户使用。
所以说在 stdarch 上再做一层抽象,让用户感知不到这些差别,可以使用普通的函数,各种常见的运算符来使用 SIMD 加速功能;但是目前社区中这一部分还不够完善,如果想使用 Rust SIMD 特性的话,只能使用简单的四则运算、位运算、比较判断、多平台通用的 API,如果还有较复杂应用的场景,则仍然 stdarch 提供的专用指令接口。这就是 Rust SIMD 语言的整体架构设计。
对比一下其他语言
C:用户代码直接调用 LLVM
go:直接嵌入汇编
Python:通过第三方库间接使用
而 Rust 则是让用户通过调用标准库接口的方式,以相对低的成本自由的使用 SIMD 加速特性。
stdarch 本身存在着大量类似的条件编译代码。因此相应的指令集模块只有在满足环境的需求时才可用。比如 x86_64 架构下可以使用 use std::arch::x86_64 语句,却不能使用 use std::arch::x86_64 或者 use std::arch::arm 语句。
社区重点工作——专业指令加速库 stdarch
专用指令加速库:https://github.com/rust-lang/stdarch
stdarch 以模块化的形式集成了各架构下常用的 SIMD 指令集,并以函数接口的形式暴露给用户使用。整个 stdarch 是集成在 Rust 标准库里的,所以开发者不用做多余的工作,只需要像使用标注库一样使用一条 use 语句引入一个相关的模块就可以了。
但记得根据自己的 CPU 开发环境进行模块的选择。如果业务代码要在多平台迁移使用,那么可以像图中实例一样,在 use 语句中加上一条条的宏语句,以此来进行条件编译,根据不同的 CPU 架构,让编译器判断在编译时引入哪一个模块。