译者 | 陈峻
51CTO读者成长计划社群招募,咨询小助手(微信号:TTalkxiaozhuli)
不知您是否了解单指令流多数据流,也就是我们常听说的SIMD(Single Instruction Multiple Data)?它是采用单个控制器来控制多个处理器,同时对一组数据中的每一个分别执行相同的操作,从而实现空间上的并行性的一种技术 。就SIMD的工作原理而言,我们可以将其理解为三个层次:
- 编译器具有一定智能,可以自动矢量化(auto-vectorize)所有的代码 。
- 编译器自动向量化的能力欠佳,容易受不相关代码变更的影响,因此开发人员需要手动编写明确的SIMD指令 。
- 需要重复为每个不同的CPU架构手工编写SIMD 。此时,作为工具的编译器,可以通过更好的指令和约束,以适合向量化的形式,编写出可靠的向量化代码 。
1、从编译器的视角看问题首先,让我们来了解编译器是如何查看代码的 。鉴于有着许多结构上相似之处,我们可以参照WebAssembly规范(https://webassembly.Github.io/spec/core/)来了解编译器在优化方面的核心规范 。如下方简单代码段所示,一个优化单元往往就是一个函数:
fn sum(xs: &[i32]) -> i32 {let mut total = 0;for i in 0..xs.len() {total = total.wrApping_add(xs[i]);}total}在一些伪IR(指令寄存器)中,上述代码表现为:fn sum return i32 {param xs_ptr: ptrparam xs_len: sizelocal total: i32local i: size = 0local x: i32local total: i32 = 0loop:branch_if i >= xs_len :retload x base=xs_ptr offset=iadd total xadd i 1goto :loopret:return total}此处出现了两个重要的特征实体:- 作为粗略字节数组的程序内存,编译器往往无法很好地推断出内存中的内容 。而且由于它们是由所有函数共享的,因此不同的函数可能会以不同的方式去解释内存中的内容 。
- 作为整数形式的局部变量,它们遵循编译器可推理的数学属性 。
param n: u32local i: u32 = 0local total: u32local tmploop:branch_if i >= n :retset tmp imul tmp 4add t tmpgoto :loopret:return total它可以推断在每一次循环中,tmp能够保持i * 4,进而会将其优化为:param n: u32local i: u32 = 0local total: u32local tmp = 0loop:branch_if i >= n :retadd t tmpadd tmp 4# replace multiplication with additiongoto :loopret:return total不过,如果我们进行相同的计算,但所有数字都位于内存中,那么编译器将很难推断出转换的正确性 。如果针对n的存储和total实际是重叠的,而tmp与某些不在当前函数中的数据重叠了,该怎么办呢?其实,load和store指令可以作为数学局部变量和内存字节的桥梁 。也就是说,load指令在内存中获取一系列字节,将字节解释为整数,并将该整数存储到局部变量中 。而store指令的执行操作正好相反 。通过将某些内容从内存加载到本地,编译器可以获得对其进行精确推理的能力 。因此,编译器无需跟踪内存的基本内容,只需检查在特定时间点,从内存进行的加载是否正确即可 。可见,编译器一次只能真正推理一个函数,而且只能推理该函数中的局部变量 。
2、将代码向编译器推近些通过为编译器提供更多的上下文,我们可以实现两项核心的优化任务:
第一项核心优化是内联(inlining) 。它使用被调用者去代替特定的调用 。据此,调用者和被调用者的局部变量都在同一个框架中,编译器可以一起优化它们 。
让我们看一段Rust代码:
fn sum(xs: &[i32]) -> i32 {let mut total = 0;for i in 0..xs.len() {total = total.wrapping_add(xs[i]);}total}其中的表达式xs[i]实际上是一个函数调用 。索引函数在访问数组元素之前,会进行边界检查 。将其内联到sum中后,编译器可以看到其代码并消除之 。毕竟,在内联之后,函数倾向于处理一般情况,并在特定的调用站点(call-site),使用足够的约束,来消除各种边缘情况 。第二项核心优化是聚合的标量替换(scalar replacement of aggregates,SROA) 。我们使用load避免对内存进行推理,而是对本地进行推理 。
例如,有如下这样一个函数:
fn permute(xs: &mut Vec<i32>) {...}
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 图解DHCP协议,搞懂你的电脑/手机如何自动获取IP的
- 我们一起聊聊MySQL中的游标,你学会了吗?
- 车辆免检后,接下来需要做什么?手把手教你→
- 书房这样设计,让你的生活更有品质
- 养一台比亚迪海豹一年需要多少钱?算出来你还会考虑燃油车吗?
- 被行政拘留过还可以开无犯罪证明吗?
- 快充、闪充、无线充?一字之差相差千里!别让你的手机提前报废
- 微信wxid被泄露:如何保护自己的账号安全?
- 你的抖音还有一直没流量吗?干货教你玩转抖音精髓!
- 抖音feed流广告与信息流广告区别有哪些?
