游戏开发论坛

 找回密码
 立即注册
搜索
查看: 13980|回复: 6

高效率的3D图形数学库(2)---SSE与矩阵相乘

[复制链接]

30

主题

357

帖子

388

积分

中级会员

Rank: 3Rank: 3

积分
388
QQ
发表于 2007-5-30 23:01:00 | 显示全部楼层 |阅读模式
这次将介绍SSE扩展指令集,以及矩阵乘法的优化,不喜汇编者请发送WM_CLOSE消息!!
闲话就不说了,SSE指令的历史到处都是,主要说说我对指令集的原理、作用和用法的理解。

SSE指令集的最大有点就是能够 4个float并行运算,与其说这恰好符合图形算法,到不如说就是为图形编程设计的。比如说,两个向量相加,x1+x2, y1+y2, z1+z2, w1+w2。用了4条加法指令,不如来一条省事。这就是SSE能为我们做的。这里要介绍8个寄存器:xmm0, xmm1, xmm2...xmm6, xmm7,这些寄存器都是128位的,每个寄存器都可以存放4个float,所以,x1, y1, z1, w1这4个float型可以放在xmm0中,而x2, y2, z2, w2可以放在xmm1中,于是 xmm0 += xmm1,结果就保存在xmm0寄存器了!下面是代码:

struct Vector
{
       float x, y, z, w;

       void Add(const Vector* pIn)
       {
          _asm
          {
             mov  eax, pIn;   // 这里其实应该是 mov eax, dword ptr[pIn];
             mov  ecx, this;

             movups  xmm0, [ecx];   // 把this的xyzw放入xmm0
             movups  xmm1, [eax];   // 把pIn的xyzw放入xmm1
             addps   xmm0, xmm1;    // xmm0 += xmm1
             movups  [ecx], xmm0;     // 把xmm0的值给this
             mov     [ecx+12], 3F800000h    // 别忘了给w = 1.0f
          }
       }
}

其实也就这么简单,movups是把内存中的向量放入寄存器,或者把寄存器中的向量放回内存,总之是一次移动128个位的一条指令,这个指令比普通的MOV指令要慢70%左右,比浮点乘法要慢的多了,大量的时间花在了movups上。所以说,简单的算法不值得去用SSE指令。

这个向量加法指令很可能会比下面的算法更慢:
void Add(const Vector* pIn)
{
    x += pIn->x;
    y += pIn->y;
    z += pIn->z;
    w += pIn->w;
}


那么,如果我要让一个向量的xyzw这4个float同时去加上同一个float值怎么办呢?比方说,我要让在xmm0中的xyzw同时加上float型变量h,就会像下面这样去做:

float h = ...;
_asm
{
        movss  xmm1, h
        shufps xmm1, xmm1, 0
        addps  xmm0, xmm1
}

好了,出现2个新的指令: movss和shufps。

movss是将一个32位的值移动到一个128位寄存器的低32位中。也就是说,这时xmm1中的4个32位区块中只有第0个区块是存放了h的值。

这时,我想让其他3个区块也存放同样的值,以便对xmm0进行并行加法处理,于是用到了下面的指令————

shufps是用来将128位寄存器中4个区块的数值进行相互调换、拷贝或覆盖,就像洗牌一样,所以叫做洗牌指令。我们现看该指令的第3个操作数是 0,该操作数是8位的,0 = 00 00 00 00,你看,我把8位数分成了4份,每一份都表示了一个寄存器区间。
00就是第0个区块
01就是第1个区块
10就是第2个区块
11就是第3个区块

shufps  dest, src, 00 00 00 00
我们将根据第3个操作数,从src中选取区间,并把该区块中的数给拖到dest相应的区块内。这里的操作数4个都是0,所以,dest的4个区块中存放的都是src中第0个区间的值。而dest和src是同一个寄存器的时候,就会是自我洗牌。于是xmm1中的4个区块存放的都是xmm1第0个区块的值。下面的示例会帮助你更好的理解这个问题:

dest =  w1, z1, y1, x1
src  =  w2, z2, y2, x2

经过该指令: shufps  dest, src, 01 11 00 10
得到该结果: dest =  y2, w2, x2, z2
看下面的会更清晰:

dest第3区块 <<------ src中01区块(1)
dest第2区块 <<------ src中11区块(3)
dest第1区块 <<------ src中00区块(0)
dest第0区块 <<------ src中10区块(2)

注意:这里的第3操作数所代表的dest寄存器区块顺序是3210,而不是0123,x通常存放在0区块,内存中地址越小的就放在编号越小的区块~~总之别给搞反了就好,这里的确很容易混淆。

而这里其实有一个万恶的限制!!!:如果dest和src是不同的寄存器,那么3、4区块的值会取dest自己区块中的,而不是src区块中的值。所以,上面执行过的dest实际是这样的:
dest =  y1, w1, x2, z2

dest第3区块 <<------ dest中01区块(1)
dest第2区块 <<------ dest中11区块(3)
dest第1区块 <<------ src中00区块(0)
dest第0区块 <<------ src中10区块(2)

如果还没明白的话.....  可以发论坛中的短消息给我。


SSE基本指令的计算有“加减乘除”四则运算,分别是: addps, subps, mulps, divps。很好记的,都是x86基本指令后面加个ps后缀。如果是ss后缀,则代表只有第0个区间做运算,速度会比ps的快20%左右,在AMD的CPU上会比浮点指令要慢,所以,传说中的性能提升400%就是纯属口胡,因为那只是理论值。但是300%还是可以做到的,特别是复杂的算法就更容易做到,比如说矩阵相乘。

现在发一段矩阵相乘的代码,出于是重点介绍SSE指令用法的缘故,主要注意了代码的可读性,所以没有对指令进行重排,所以会比我的最优化版慢25%左右的速度,但即便如此,已然是一般算法的3倍速了,看官们可以考回去实验一下,重排后的代码会在以后放出。

void MultMatrix(const Matrix* pOut, const Matrix* pIn1, const Matrix* pIn2)
{
        if (!g_bUseSSE2)
        {
                // [edx]   =   xmm0   *   xmm4    +    xmm1   *   xmm5    +    xmm2  *   xmm6   +    xmm3   *   xmm7
                pOut->_11 = pIn1->_11*pIn2._11 + pIn1->_12*pIn2._21 + pIn1->_13*pIn2._31 + pIn1->_14*pIn2._41;
                pOut->_12 = pIn1->_11*pIn2._12 + pIn1->_12*pIn2._22 + pIn1->_13*pIn2._32 + pIn1->_14*pIn2._42;
                pOut->_13 = pIn1->_11*pIn2._13 + pIn1->_12*pIn2._23 + pIn1->_13*pIn2._33 + pIn1->_14*pIn2._43;
                pOut->_14 = pIn1->_11*pIn2._14 + pIn1->_12*pIn2._24 + pIn1->_13*pIn2._34 + pIn1->_14*pIn2._44;

                pOut->_21 = pIn1->_21*pIn2._11 + pIn1->_22*pIn2._21 + pIn1->_23*pIn2._31 + pIn1->_24*pIn2._41;
                pOut->_22 = pIn1->_21*pIn2._12 + pIn1->_22*pIn2._22 + pIn1->_23*pIn2._32 + pIn1->_24*pIn2._42;
                pOut->_23 = pIn1->_21*pIn2._13 + pIn1->_22*pIn2._23 + pIn1->_23*pIn2._33 + pIn1->_24*pIn2._43;
                pOut->_24 = pIn1->_21*pIn2._14 + pIn1->_22*pIn2._24 + pIn1->_23*pIn2._34 + pIn1->_24*pIn2._44;

                pOut->_31 = pIn1->_31*pIn2._11 + pIn1->_32*pIn2._21 + pIn1->_33*pIn2._31 + pIn1->_34*pIn2._41;
                pOut->_32 = pIn1->_31*pIn2._12 + pIn1->_32*pIn2._22 + pIn1->_33*pIn2._32 + pIn1->_34*pIn2._42;
                pOut->_33 = pIn1->_31*pIn2._13 + pIn1->_32*pIn2._23 + pIn1->_33*pIn2._33 + pIn1->_34*pIn2._43;
                pOut->_34 = pIn1->_31*pIn2._14 + pIn1->_32*pIn2._24 + pIn1->_33*pIn2._34 + pIn1->_34*pIn2._44;

                pOut->_41 = pIn1->_41*pIn2._11 + pIn1->_42*pIn2._21 + pIn1->_43*pIn2._31 + pIn1->_44*pIn2._41;
                pOut->_42 = pIn1->_41*pIn2._12 + pIn1->_42*pIn2._22 + pIn1->_43*pIn2._32 + pIn1->_44*pIn2._42;
                pOut->_43 = pIn1->_41*pIn2._13 + pIn1->_42*pIn2._23 + pIn1->_43*pIn2._33 + pIn1->_44*pIn2._43;
                pOut->_44 = pIn1->_41*pIn2._14 + pIn1->_42*pIn2._24 + pIn1->_43*pIn2._34 + pIn1->_44*pIn2._44;
        }
        else
        {
                _asm
                {
                        mov        edx, pIn2;                // 这时保存的是pIn2
                        movups        xmm4, [edx];        //pIn2的第1行
                        movups        xmm5, [edx+16];        //pIn2的第2行
                        movups        xmm6, [edx+32];        //pIn2的第3行
                        movups        xmm7, [edx+48];        //pIn2的第4行

                        mov eax, pIn1;                // 这时保存的是pIn1
                        mov        edx, pOut;

                        mov        ecx, 4;                        // 循环4次

LOOPIT:                // 开始循环
                        movss        xmm0, [eax];        xmm0 = pIn1->x
                        shufps        xmm0, xmm0, 0;        洗牌xmm0 = pIn1->x, pIn1->x, pIn1->x, pIn1->x
                        mulps        xmm0, xmm4;

                        movss        xmm1, [eax+4];        xmm1 = pIn1->y
                        shufps        xmm1, xmm1, 0;        洗牌xmm1 = pIn1->y, pIn1->y, pIn1->y, pIn1->y
                        mulps        xmm1, xmm5;

                        movss        xmm2, [eax+8];        xmm2 = pIn1->z
                        shufps        xmm2, xmm2, 0;        洗牌xmm2 = pIn1->z, pIn1->z, pIn1->z, pIn1->z
                        mulps        xmm2, xmm6;

                        movss        xmm3, [eax+12];        xmm3 = pIn1->w
                        shufps        xmm3, xmm3, 0;        洗牌xmm3 = pIn1->w, pIn1->w, pIn1->w, pIn1->w
                        mulps        xmm3, xmm7;

                        addps        xmm0, xmm1;
                        addps        xmm2, xmm3;
                        addps        xmm0, xmm2;                最终结果行保存在xmm0

                        movups        [edx], xmm0;        将结果保存到pOut中
                        add                edx, 16;
                        add                eax, 16;                作为变址用

                        loop        LOOPIT;
                }
        }
}

这里的汇编对照上面的一般算法就很容易理解。下回我会说说矩阵类的关键算法。

To Be Continued....... [em5] [em5] [em5]

30

主题

357

帖子

388

积分

中级会员

Rank: 3Rank: 3

积分
388
QQ
 楼主| 发表于 2007-5-31 18:48:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

给源代码的帖子居然也沉的这么快...... 唉~
都没人研究这个么?

59

主题

1104

帖子

1199

积分

金牌会员

Rank: 6Rank: 6

积分
1199
发表于 2007-5-31 18:58:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

sse2...不说了,当sse2碰到AltiVec128就象那蚂蚁碰到了大象....
VMX128不是一般的强.

6

主题

99

帖子

99

积分

注册会员

Rank: 2

积分
99
发表于 2007-5-31 23:30:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

顶,支持lz。
研究研究。

8

主题

46

帖子

52

积分

注册会员

Rank: 2

积分
52
发表于 2007-6-1 02:24:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

lz要是能给出一些参考资料就好了~~加油!

0

主题

1

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2007-7-5 08:20:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

谢谢,通俗易懂

13

主题

49

帖子

60

积分

注册会员

Rank: 2

积分
60
发表于 2007-8-18 20:05:00 | 显示全部楼层

Re:高效率的3D图形数学库(2)---SSE与矩阵相乘

个人觉得利用 英特尔 SIMD 流指令扩展指令集,优化动画模型的渲染管线
作者 Id Software 公司,J.M.P. van Waveren
http://www.intel.com/cd/ids/developer/apac/zho/295264.htm
挺有意思的
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2026-1-25 10:33

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表