设计快速跨平台SIMD矢量库

大部分3D应用中都有执行程序计算的矢量库,比如矢量运算,逻辑,比较,点和乘积等。尽管有无数设计这类库的方法,开发者们还是会经常忽略让这种矢量库以最快速度计算的关键要素。

大概2004年晚些时候,我接到一项任务,开发命名为VMath的矢量库,VMath代表的意思是“矢量数学(Vector Math)。”VMath的主要目标不仅仅在于最快速的运算,同时还要让它易于在不同平台之间移植。

2009年,令我惊讶的是,电脑编程技术并未改变多少。实际上本文中的结论,正是我在那时候的研究结果,让人意外的是,这几乎同我五年前开发VMath的时候一模一样。

“最快速”的库

由于本文大部分内容是以C++语言为主,且注重性能,有关最快速的库定义有可能引起歧义。

我们将最快速的库描述为,当在同样设置(当然是发布模式)下编译同样的代码时,同其他的库相比,这一代码库生成的汇编代码是最少的。这是因为最快速的库生成更少的指令来履行同样的精确运算。换句话说,最快速的库的代码膨胀是最少的。

SIMD 介绍

随着单指令多数据流(Single Instruction Multiple Data, SIMD)在处理器上的广泛应用,开发矢量库更为简单了。SIMD寄存器上的SIMD操作就像FPU寄存器上的FPU操作一样精确。尽管如此,SIMD的优势在于,SIMD寄存器通常以128位宽形成四字:四个“浮点数”或“整数”,每个32位。这让开发者们能够在单一指令下履行4D矢量运算。正因为这样,矢量库中最好的功能就是它的SIMD指令。

虽然如此,在使用SIMD指令的时候,你必须堤防能够引起库代码膨胀的普遍错误。实际上一个用SIMD方式实现的矢量库中的代码膨胀问题会在某个点激化,这样反而不如直接使用FPU指令。

矢量库接口

当设计矢量库高级接口时,运用SIMD的最佳方式是使用内建。对于大多数面向支持SIMD指令的处理器的编译器,它们都可用。而且,每个内建指令都转换为单一SIMD指令。不管怎样,使用内建指令代替直接汇编的优势在于编译器能够执行调度和表达式优化。这可以极大地将代码膨胀降到最低。

以下为内建示例:

Intel & AMD:

vr = _mm_add_ps(va, vb);

Cell Processor (SPU):

vr = spu_add(va, vb);

Altivec:

vr = vec_add(va, vb);

1.以数值形式返回结果

通过观察内建接口,矢量库必须模拟这些接口来实现性能最大化。因此,必须以数值形式返回结果而不是以参数形式返回结果,像这样:

//correct
inline Vec4 VAdd(Vec4 va, Vec4 vb)
{

return(_mm_add_ps(va, vb));

};

换句话说,如果数据以参数形式返回将导致代码膨胀。错误示例如下:

//incorrect (code bloat!)
inline void VAddSlow(Vec4& vr, Vec4 va, Vec4 vb)
{

vr = _mm_add_ps(va, vb);

};

必须以数值返回数据的原因是,四字(128位)在SIMD寄存器中为最适合。矢量库的一个关键要素在于尽可能的在这些寄存器中保存数据。通过这样做,可以避免SIMD寄存器到内存或FPU寄存器的不必要的载入和存储操作。当合并多项矢量操作时,“以数值返回”接口使编译器能很容易地通过最小化SIMD向FPU或内存传输来优化这些载入和存储。

2.“纯数据”声明

这里,“纯数据”定义为,在“class”或“struct”之外,简单地使用“typedef”或“define”进行的数据声明。当我在编写VMath之前调查各类矢量库时,我观察到所有的库中的通用样式。在所有案例中,开发者们将基础四字打包入“class”或“struct”而不是单纯地声明它,如下:

class Vec4
{

...
private:
__m128 xyzw;

};

这一类型的数据封装在C++开发者的实践中甚为普遍,用来让软件结构更具健壮性。数据被定义为保护的且只能通过类接口函数来访问。但是,这样的设计导致了不同平台上的不同编译器引起的代码膨胀,尤其是在使用GCC这类编译器移植的时候。

对于编译器一种更为友善的方法是“纯粹的”声明矢量数据,如下:

typedef __m128 Vec4;

不可否认,那样设计矢量库会失去对基础数据的好的封装和保护。虽然如此,还是取得了显著成果。让我们看一看澄清这一问题的例子:

我们可以使用Maclaurin (*)级数(麦克劳林级数)估算出正弦函数,如下:


(*)在制作代码的过程中有很多更好更快的方式来估算出正弦函数。这里使用Maclaurin只是为了举例说明。

如果开发者运用以上公式编写正弦函数的矢量版本代码,那么代码差不多会是这样:

Vec4 VSin(const Vec4& x)
{

Vec4 c1 = VReplicate(-1.f/6.f);
Vec4 c2 = VReplicate(1.f/120.f);
Vec4 c3 = VReplicate(-1.f/5040.f);
Vec4 c4 = VReplicate(1.f/362880);
Vec4 c5 = VReplicate(-1.f/39916800);
Vec4 c6 = VReplicate(1.f/6227020800);
Vec4 c7 = VReplicate(-1.f/1307674368000);

Vec4 res = x +

c1*x*x*x +
c2*x*x*x*x*x +
c3*x*x*x*x*x*x*x +
c4*x*x*x*x*x*x*x*x*x +
c5*x*x*x*x*x*x*x*x*x*x*x +
c6*x*x*x*x*x*x*x*x*x*x*x*x*x +
c7*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x;


return (res);

}

现在让我们看看同一函数的汇编代码,左列是使用纯数据声明Vec 4方式编译后的代码,右列是将数据定义在类内部编译后的代码。(点击这里下载表格文件。参照Table 1)

简单的改变基础数据声明方式,这同一段代码编译后就缩减了大约15%。如果这一函数在循环内部执行运算,这就不仅是节约了代码量,显然会加快运行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值