改进D数学的性能
原文
有关LDC1.1.0
中新的@fastmath
和__traits(target*)
功能的文章,这里.
@fastmath
属性放宽了浮点数学
约束,Mir
用它来击败OpenBLAS
和Eigen
.
为了避免混淆:LLVM
做有趣的工作,LDC
只是增加了一些部分来让LLVM
发挥它的神力.
D的数字年龄
CPU
中可用的SIMD
并行性非常强大
:借助AVX512
指令集,CPU
在一个寄存器中封装了16或32
位浮点数,并且每个
时钟周期可执行两次SIMD
乘加法.因此,针对性能
编程,归结为编写编译器可尽量利用SIMD
的代码.
mir更快文章,本文介绍了他对Mir
的线性代数添加的性能基准,Mir
是用D
编写的通用数字库
,证明他的D代码
速度很快,并且击败了有相同功能的知名
汇编语言实现.
为了获得非常高的性能,基本数学
原语函数的常见实现是用汇编
语言显式
编写的,而不是信任
编译器生成同样快
的机器代码.相比之下,Ilya
写道:"MirGLAS
(通用线性代数
子程序)有个适合
所有CPU
目标,所有浮点
类型和复
类型的通用内核
.
它完全用D编写,没有汇编块
.这是个相当大
的成就:精心优化
的D代码的执行速度
与精心优化
的汇编
代码一样快.在本文中,介绍一些LDC
的(简单)补充,使LLVM
的神奇优化器可为Ilya
惊人的D代码
生成如此快速的汇编代码.
汇编代码
我使用-c-output-s
,由LDC
展示生成的汇编代码片.需要LDC
版本1.1.0-beta2
或更高版本.你可用ldc.acomirei.ru
在线编译器,查看LDC
生成的汇编代码
.
如果发现D代码
生成的汇编
可改进,请在Github
上提交LDC
问题.
浮点数学
CPU
的浮点
数学与代数
数学不同,因为每次
计算后的有限可用的浮点数
存储,因此会出现近似误差
.计算a+(b+c)
可能等于也可能不等于(a+b)+c
,并且(a+a)-a
可能等于也可能不等于a
.
使用(FMA)
融乘加,a*b+c
与a*b+c
不同.这一般会阻止优化数学
代码,除非
告诉编译器,对他
来说都是一样的.
看看基本
数学函数,看看如何改进
编译器生成的机器代码
.
向量点积
以下代码
计算任意长度的两个向量
点积:
double dot(double[] a, double[] b) {double s = 0;foreach (size_t i; 0 .. a.length) {s += a[i] * b[i];}return s;
}
点积
的代数方程为:
s = (a1 * b1) + (a2 * b2) + (a3 * b3) + ...
但因为浮点
数学的不准确性,必须如下描述D
代码计算:
s = ((((0 + a1 * b1) + a2 * b2) + a3 * b3) + ... )
括号
很重要:它们表示操作的顺序
,对结果
绝对至关重要.从该等式中,可见乘法
可按任意
顺序并行
完成,但加法
必须按顺序
且为非常特定
顺序来完成.
看看LDC1.1.0
生成了什么汇编代码(AT&T
方言).我只展示
点函数的实质,在本例中是4路折叠展开
的内部循环:
ldc2 -c -output-s -O3 -release dot.dLBB0_4:movsd (%rcx,%rdi,8), %xmm1mulsd (%rsi,%rdi,8), %xmm1addsd %xmm0, %xmm1movsd 8(%rcx,%rdi,8), %xmm0mulsd 8(%rsi,%rdi,8), %xmm0addsd %xmm1, %xmm0movsd 16(%rcx,%rdi,8), %xmm1mulsd 16(%rsi,%rdi,8), %xmm1addsd %xmm0, %xmm1movsd 24(%rcx,%rdi,8), %xmm0mulsd 24(%rsi,%rdi,8), %xmm0addsd %xmm1, %xmm0addq $4, %rdicmpq %rdi, %rdxjne LBB0_4
没有向量化
:它只是顺序乘加
,乘加
,…
但是等等,我忘了告诉编译器我的CPU
比默认的core2CPU
更新(注意,至少在OSX
上,默认LDC
会为10
年前的处理器
编译你的代码).此时,这并不重要:
ldc2 -c -output-s -O3 -release dot.d -mcpu=haswell
...vmovsd 24(%rcx,%rdi,8), %xmm2vmulsd 24(%rsi,%rdi,8), %xmm2, %xmm2vaddsd %xmm1, %xmm0, %xmm0vmovsd 32(%rcx,%rdi,8), %xmm1vmulsd 32(%rsi,%rdi,8), %xmm1, %xmm1vaddsd %xmm2, %xmm0, %xmm0
...
不要被似乎
暗示"向量"
的"v"
所迷惑.这些乘法和加法
指令只是单个双精
运算.循环是展开
了8个折叠
,这就是全部变化
.不放松数学
限制时,这是最好
的.
融乘加
一项性能
改进是允许编译器将a*b+c"
融合"到一条fma(a,b,c)
指令中.它改变计算
结果,但对许多
用例来说,这并不重要.注意:使用FMA
的结果不一定比以前差(它可能更准确),只是不同
,因此
编译器不能默认使用FMA
的原因.
可如下指示LLVM
允许使用LDC
属性
@llvmAttr("unsafe-fp-math", "true")
来允许"unsafe-fp-math"
,如FMA
(不建议使用此低级属性
).
import ldc.attributes : llvmAttr;
@llvmAttr("unsafe-fp-math", "true")
double dot(double[] a, double[] b) {double s = 0;foreach (size_t i; 0 .. a.length) {s += a[i] * b[i];}return s;
}
ldc2 -c -output-s -O3 -release dot.d -mcpu=haswell
...vmovsd (%rcx,%rdi,8), %xmm1vfmadd132sd (%rsi,%rdi,8), %xmm0, %xmm1vmovsd 8(%rcx,%rdi,8), %xmm0vfmadd132sd 8(%rsi,%rdi,8), %xmm1, %xmm0
...
LLVM
用非向量化
的融乘加
指令取代了乘法和加法
指令,显然是一场胜利.但是代码
仍没用CPU
的向量化指令集
.
向量化和@fastmath
向量化
代码,还缺少最后一点:告诉LLVM
,可任意操作浮点运算
:如,代码不必支持NaN
或无穷大
,可改变点积
中加法
操作的顺序.
LLVM
的快速数学
这里标志就是为此,你可用LDC
的@llvmFastMathFlag
属性(低级,不推荐),将其应用至函数
中的所有浮点运算
.合并@llvmAttr
和@llvmFastMathFlag
到@fastmath
属性中.
建议使用@fastmath
,而不是其他两个
低级属性;它对LLVM
更改的弹性要强
得多.ldc
确保使@fastmath
与LLVM
要求的属性一起工作.
有了@fastmath
结果就变成了:
import ldc.attributes : fastmath;
@fastmath
double dot(double[] a, double[] b) {double s = 0;foreach (size_t i; 0 .. a.length) {s += a[i] * b[i];}return s;
}
ldc2 -c -output-s -O3 -release dot.d -mcpu=haswellLBB0_6:vmovupd (%rsi,%rdi,8), %ymm4vmovupd 32(%rsi,%rdi,8), %ymm5vmovupd 64(%rsi,%rdi,8), %ymm6vmovupd 96(%rsi,%rdi,8), %ymm7vfmadd132pd (%rcx,%rdi,8), %ymm0, %ymm4vfmadd132pd 32(%rcx,%rdi,8), %ymm1, %ymm5vfmadd132pd 64(%rcx,%rdi,8), %ymm2, %ymm6vfmadd132pd 96(%rcx,%rdi,8), %ymm3, %ymm7vmovupd 128(%rsi,%rdi,8), %ymm0vmovupd 160(%rsi,%rdi,8), %ymm1vmovupd 192(%rsi,%rdi,8), %ymm2vmovupd 224(%rsi,%rdi,8), %ymm3vfmadd132pd 128(%rcx,%rdi,8), %ymm4, %ymm0vfmadd132pd 160(%rcx,%rdi,8), %ymm5, %ymm1vfmadd132pd 192(%rcx,%rdi,8), %ymm6, %ymm2vfmadd132pd 224(%rcx,%rdi,8), %ymm7, %ymm3addq $32, %rdiaddq $2, %raxjne LBB0_6
现在有个很好的向量化点积函数
:vfmadd132pd
指令执行向量化融乘加运算
,此时,它相当于4个
非向量化运算.每次循环迭代
处理32
个向量元素.
所有D代码
,与目标无关
上例中,D
代码没有指定目标
架构:都是自动向量化
的.功劳必须归功于LLVM
令人惊叹的后端
,它可很好地分析和优化
代码.因此,当你用更宽SIMD
寄存器为新处理器
编译时,只需要针对该CPU
重新编译,D
代码就会更快:
ldc2 -c -output-s -O3 -release dot.d -mcpu=knl
导致一次处理8个矢量元素的vfmadd231pd(%rcx,%rdi,8),%zmm4,%zmm0
指令.knl
为Intel的Knights Landing
.
如果目标是带NEONSIMD
技术的ARM
处理器,会获得自动向量化FMA
代码:
ldc2 -c -output-s -O3 -release dot.d -mtriple=aarch64-none-linux-gnu -mattr=+neon.LBB0_5:ldp q2, q3, [x9, #-16]ldp q4, q5, [x10, #-16]add x9, x9, #32add x10, x10, #32sub x11, x11, #4fmla v0.2d, v4.2d, v2.2dfmla v1.2d, v5.2d, v3.2dcbnz x11, .LBB0_5
特征
上面的点积
示例是个非常简单
的函数.对更复杂
算法,如果考虑CPU
缓存大小,则可获得更好
性能;为调优
性能,指令和数据缓存
的特性都很重要.
LDC1.1.0
有两个特殊的提供
有关编译目标
架构编译时信息的__traits
:__traits(targetHasFeature,"...")
和__traits(targetCPU)
.如,__traits(targetHasFeature,"avx2")
,根据编译目标
是否支持AVX2
指令,返回true
或false
.
Mir
使用__traits(targetHasFeature,"...")
来决定算法
中使用哪些向量类型
(如,4
或8个浮点数的向量).