【云驻共创】 通过Rust语言计算加速技术突破图片识别性能瓶颈
0 前言
Rust 是一门享誉中外的安全和高效的系统编程语言,业界各大平台包括华为选择和引入 Rust 这门语言作为自己的产品的开发语言。
华为在图片的识别算法中验证了这门语言的安全和高效性。并使用高级计算加速技术使其达到了效果倍增的效果,并超过了使用 C 语言实现得到的最好效果,而 SIMD 技术(单指令流多数据流)发挥了最重要的作用。
本文以图片脏污检测算法优化案例为基础,主要介绍 Rust 中一些以 SIMD 技术为主的计算加速类应用,希望对大家在今后的学和开发过程中有所帮助。
1 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 程序做的更好。
2 什么是 SIMD 技术
SIMD,英文全称 Single Instruction Multiple Data ,中文翻译为单指令多数据流。一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
SIMD 是一种基于特殊 CPU 计算单元的性能优化技术。顾名思义,指的是在一条 CPU 指令执行期间可以执行多条数据的计算。主要运用于科学计算、多媒体处理等数据密集型运算场景下。
借助于此种方法,一般可达到数倍甚至数十倍的性能提升。
比如左图,对于连续的加法运算,传统的实现是将数据依次进行每对操作数的加法指令。而 SIMD 技术会依次读取多个数据,组成一对 SIMD 加法向量,放入特殊的向量计算器中,然后使用专门的 CPU 指令计算出这对向量的和。
这个特殊的 CPU 指令主要靠下图中的 SIMD 处理单元结构来实现的。
目前主流的 CPU 架构比如 X86、ARM、MIPS 都集成了成千上百个这样的 SIMD 处理单元,每一个都对应了一种或多种 SIMD 运算。
总的来说,SIMD 技术是一种软件中充分调用硬件性能,实现性能倍增的软硬件协同技术。
2.1 SIMD 技术业界优秀实践
这里列出了 SIMD 比较出名的业界优秀软件实践。
• Numpy:多维数组计算开发库,主流用 Python 语言做数字计算开发,它的实现中有许多地方做了 SIMD 开发优化。
• OpenCV:计算机视觉库,它的核心 Universal Intrinsics API 基本都是基于 SIMD 来实现的。
• IPP:Intel® IPP,因特尔的多媒体计算库,里面的函数实现都使用了SIMD指令进行优化。
• OpenBLAS:一个开源的矩阵计算库,包含了诸多的精度和形式的矩阵计算算法。
• KML:华为自研的数学库,鲲鹏数学库(Kunpeng Math Library,以下简称KML)是基于华为鲲鹏处理器的高性能数学计算加速库,提供了基于鲲鹏平台优化的数学函数。
可以说,只要是以计算性能为核心竞争力的软件,SIMD 都是不可绕开的核心技术。
3 Rust 语言中的 SIMD
SIMD 在 Rust 语言社区中是以 RFC 被提出,经过一两年的讨论,作为 Roadmap 路径之一进行主要的开发工作,涉及从业务代码贯穿到底层硬件架构,是一个跨度大、工作量也很大的一个特性。
3.1 架构设计
架构图设计图如下:
底层硬件架构
底层的硬件架构,每种 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 语句。
3.2 社区重点工作——专业指令加速库 stdarch
专用指令加速库:https://github.com/rust-lang/stdarch
stdarch 以模块化的形式集成了各架构下常用的 SIMD 指令集,并以函数接口的形式暴露给用户使用。整个 stdarch 是集成在 Rust 标准库里的,所以开发者不用做多余的工作,只需要像使用标注库一样使用一条 use 语句引入一个相关的模块就可以了。
但记得根据自己的 CPU 开发环境进行模块的选择。如果业务代码要在多平台迁移使用,那么可以像图中实例一样,在 use 语句中加上一条条的宏语句,以此来进行条件编译,根据不同的 CPU 架构,让编译器判断在编译时引入哪一个模块。
3.3 stdarch 的主要工作——SIMD 接口的实现
stdarch 主要有两种接口实现方式
相同点:都是让用户可以用一般函数调用和传参的方式使用 SIMD 接口,其中需要使用编译器和 LLVM
区别:交互方式
第一种:通过 FFI 静态链接的方式,生成特定的函数签名,由编译器将其翻译成正确的 SIMD LLVM IR 并在 LLVM 生成汇编室,根据 IR 终极静态链接填入相应的汇编指令集。
这种方法和 C 中使用的方法比较类似,相对来说比较直接。缺点:需要开发者对 LLVM 有一定的使用经验,而且对 LLVM IR 有严格的使用要求。
由于 Rust 编译器本身会内置部分通用的 SIMD 指令,如加减乘除、位运算、元素重排等等;通过关键 platform 和 intrinsic 进行使用。使用可以有多条这样的指令组合成可以由 LLVM 的自动向量优化机制识别的一些组合接口。
使用用例:
下面是使用 SIMD 计算 0 到 len 中所有整数点的平方根之和
其他配套工具:
• 硬件特性检测 std-dect
• 自动化函数生成工具 stdarch-gen
• 接口验证工具 stdarch-verify
3.4 Rust 中 SIMD 多平台适配库
portable-simd 是多架构通用加速库:http://github.com/rust-lang/portable-simd
发展历程:packed_simd –> packed_simd2 –> portable-simd
编译器提供 platform-intrinsics
然后定义了一套 SIMD 数据结构和方法:sum()、reduce() 、select() 和运算符重载
使用示例:
4 Rust SIMD 的展望
1. portable-simd 功能的完善
2. 自动向量长度与指令选择
3. 指令集扩展
4. 新架构支持
5. 丰富、场景化的 API 接口
6. 自组合 SIMD 函数
5 其他计算加速技术
5.1 其他计算加速技术:迭代器
• 避免越界检测
• 循环展开
• 惰性计算
• 更好的编辑器 pass 优化
5.2 其他计算加速技术:并行化
• rayon:一行代码实现并行化
缺点:对迭代器类型和被迭代变量的要求较严格
• async/await、tokio 异步计算技术
5.3 其他计算加速技术:编译器优化技术
• 内联汇编
• MIR 优化
• 自动向量化
• 计算图
6 高性能代码重构实践
6.1 识别重构关键点定位
• 重构关键点 1:数据存取
通过一次性读取多个数据,用以减少缓存次数,提升程序效率。
不仅仅是顺序读取,可以单次读取多数据,使用 vld4q_s32 SIMD ,比每次读取一次数据提高了 16 倍速率。
• 重构关键点 2:非前后依赖型数据计算,使用 vmlaq_s32 计算三个数组元素,获得 4 倍效率。
• 重构关键点 3:特殊场景—— AES 加解密、哈希算法、矩阵乘法、复数运算等都有专门为其开发的 SIMD 指令。可以直接使用。
6.2 重构方法
1. 指令替换
2. SIMD 接口组合
3. 计算逻辑拆分
6.3 脏污检测算法代码重构案例
实践 1:迭代器的优化
左边的例子中分别要对 res 、resPrev 、resSum 三个数组进行迭代
而右边的代码通过 zip 方法获得了三个数组的联合迭代器,然后使用 for_each 方法进行循环计算,循环内的取值和计算逻辑保持不变的情况下,效率提升了 20% 左右。
实践 2:SIMD 优化
左边是一段连续的三段代码,可以按照顺序分为三个部分,每个部分都对应了 SIMD 优化的方式:
• 红色部分是数组的访问,也就是访存指令,可以使用 SIMD 指令进行加速,一次读取多个数据
• 绿色部分是乘法和加法的计算,可以使用 SIMD 指令的加法和乘法进行接口的替换和组合
• 蓝色部分是条件判断部分,这部分需要使用布尔型向量(包含全 0 或全 1 元素)存储判断条件,这种向量中每个元素的长度和计算数据类型保持一致,然后使用选择指令实现判断分支
实践 3:运算拆分优化
接下来看一段运算拆分优化的例子,左边的代码可以分为三个部分:
• 绿色部分包含自乘计算、访存、类型转换,都可以用 SIMD 指令进行加速
• 红色部分属于累加计算,不可以用 SIMD
• 蓝色部分属于向量加计算,也可以用 SIMD 进行计算
虽然这段代码只有简单的 5 行,但我们可以通过这种方式实现了最好的优化效果。
7 总结
本文介绍了如何通过 Rust 语言计算加速技术突破识别性能瓶颈,我们都看到 Rust 是一门声名渐起的语言,有着高效和安全的特点,然后介绍 Rust 语言中的 SIMD 技术包括其架构设计、专业指令加速库 stdarch 和接口实现和未来展望。
除了 SIMD 指令加速技术,文中还介绍了其他计算加速技术:迭代器、并行化、编辑器优化技术。
最后在以实践中的例子介绍了如何编写高性能代码重构的方法和案例,希望本文的内容能对你有所帮助,谢谢!
本文参与华为云社区【内容共创】活动第17期。
- 点赞
- 收藏
- 关注作者
评论(0)