> 文章列表 > 改进D数学的性能

改进D数学的性能

改进D数学的性能

原文

有关LDC1.1.0中新的@fastmath__traits(target*)功能的文章,这里.
@fastmath属性放宽了浮点数学约束,Mir用它来击败OpenBLASEigen.
为了避免混淆: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+ca*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确保使@fastmathLLVM要求的属性一起工作.

有了@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指令.knlIntel的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指令,返回truefalse.

Mir使用__traits(targetHasFeature,"...")来决定算法中使用哪些向量类型(如,4或8个浮点数的向量).