zhigu 发表于 2007-6-1 22:09:00

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

<strong><br/><br/></strong>这次将介绍SSE扩展指令集,以及矩阵乘法的优化,不喜汇编者请发送WM_CLOSE消息!!<br/>闲话就不说了,SSE指令的历史到处都是,主要说说我对指令集的原理、作用和用法的理解。 <p></p><p>SSE指令集的最大有点就是能够&nbsp;4个float并行运算,与其说这恰好符合图形算法,到不如说就是为图形编程设计的。比如说,两个向量相加,x1+x2,&nbsp;y1+y2,&nbsp;z1+z2,&nbsp;w1+w2。用了4条加法指令,不如来一条省事。这就是SSE能为我们做的。这里要介绍8个寄存器:xmm0,&nbsp;xmm1,&nbsp;xmm2...xmm6,&nbsp;xmm7,这些寄存器都是128位的,每个寄存器都可以存放4个float,所以,x1,&nbsp;y1,&nbsp;z1,&nbsp;w1这4个float型可以放在xmm0中,而x2,&nbsp;y2,&nbsp;z2,&nbsp;w2可以放在xmm1中,于是&nbsp;xmm0&nbsp;+=&nbsp;xmm1,结果就保存在xmm0寄存器了!下面是代码:</p><p>struct&nbsp;Vector<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;float&nbsp;x,&nbsp;y,&nbsp;z,&nbsp;w;</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;void&nbsp;Add(const&nbsp;Vector*&nbsp;pIn)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_asm<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mov&nbsp;&nbsp;eax,&nbsp;pIn;&nbsp;&nbsp;&nbsp;//&nbsp;这里其实应该是&nbsp;mov&nbsp;eax,&nbsp;dword&nbsp;ptr;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mov&nbsp;&nbsp;ecx,&nbsp;this;</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;movups&nbsp;&nbsp;xmm0,&nbsp;;&nbsp;&nbsp;&nbsp;//&nbsp;把this的xyzw放入xmm0<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;movups&nbsp;&nbsp;xmm1,&nbsp;;&nbsp;&nbsp;&nbsp;//&nbsp;把pIn的xyzw放入xmm1<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;addps&nbsp;&nbsp;&nbsp;xmm0,&nbsp;xmm1;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;xmm0&nbsp;+=&nbsp;xmm1<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;movups&nbsp;&nbsp;,&nbsp;xmm0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;把xmm0的值给this<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mov&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;,&nbsp;3F800000h&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;别忘了给w&nbsp;=&nbsp;1.0f<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>}</p><p>其实也就这么简单,movups是把内存中的向量放入寄存器,或者把寄存器中的向量放回内存,总之是一次移动128个位的一条指令,这个指令比普通的MOV指令要慢70%左右,比浮点乘法要慢的多了,大量的时间花在了movups上。所以说,简单的算法不值得去用SSE指令。</p><p>这个向量加法指令很可能会比下面的算法更慢:<br/>void&nbsp;Add(const&nbsp;Vector*&nbsp;pIn)<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;x&nbsp;+=&nbsp;pIn-&gt;x;<br/>&nbsp;&nbsp;&nbsp;&nbsp;y&nbsp;+=&nbsp;pIn-&gt;y;<br/>&nbsp;&nbsp;&nbsp;&nbsp;z&nbsp;+=&nbsp;pIn-&gt;z;<br/>&nbsp;&nbsp;&nbsp;&nbsp;w&nbsp;+=&nbsp;pIn-&gt;w;<br/>}</p><p><br/>那么,如果我要让一个向量的xyzw这4个float同时去加上同一个float值怎么办呢?比方说,我要让在xmm0中的xyzw同时加上float型变量h,就会像下面这样去做:</p><p>float&nbsp;h&nbsp;=&nbsp;...;<br/>_asm<br/>{<br/>movss&nbsp;&nbsp;xmm1,&nbsp;h<br/>shufps&nbsp;xmm1,&nbsp;xmm1,&nbsp;0<br/>addps&nbsp;&nbsp;xmm0,&nbsp;xmm1<br/>}</p><p>好了,出现2个新的指令:&nbsp;movss和shufps。</p><p>movss是将一个32位的值移动到一个128位寄存器的低32位中。也就是说,这时xmm1中的4个32位区块中只有第0个区块是存放了h的值。</p><p>这时,我想让其他3个区块也存放同样的值,以便对xmm0进行并行加法处理,于是用到了下面的指令————</p><p>shufps是用来将128位寄存器中4个区块的数值进行相互调换、拷贝或覆盖,就像洗牌一样,所以叫做洗牌指令。我们现看该指令的第3个操作数是&nbsp;0,该操作数是8位的,0&nbsp;=&nbsp;00&nbsp;00&nbsp;00&nbsp;00,你看,我把8位数分成了4份,每一份都表示了一个寄存器区间。<br/>00就是第0个区块<br/>01就是第1个区块<br/>10就是第2个区块<br/>11就是第3个区块</p><p>shufps&nbsp;&nbsp;dest,&nbsp;src,&nbsp;00&nbsp;00&nbsp;00&nbsp;00<br/>我们将根据第3个操作数,从src中选取区间,并把该区块中的数给拖到dest相应的区块内。这里的操作数4个都是0,所以,dest的4个区块中存放的都是src中第0个区间的值。而dest和src是同一个寄存器的时候,就会是自我洗牌。于是xmm1中的4个区块存放的都是xmm1第0个区块的值。下面的示例会帮助你更好的理解这个问题:</p><p>dest&nbsp;=&nbsp;&nbsp;w1,&nbsp;z1,&nbsp;y1,&nbsp;x1<br/>src&nbsp;&nbsp;=&nbsp;&nbsp;w2,&nbsp;z2,&nbsp;y2,&nbsp;x2</p><p>经过该指令:&nbsp;shufps&nbsp;&nbsp;dest,&nbsp;src,&nbsp;01&nbsp;11&nbsp;00&nbsp;10<br/>得到该结果:&nbsp;dest&nbsp;=&nbsp;&nbsp;y2,&nbsp;w2,&nbsp;x2,&nbsp;z2<br/>看下面的会更清晰:</p><p>dest第3区块&nbsp;&lt;&lt;------&nbsp;src中01区块(1)<br/>dest第2区块&nbsp;&lt;&lt;------&nbsp;src中11区块(3)<br/>dest第1区块&nbsp;&lt;&lt;------&nbsp;src中00区块(0)<br/>dest第0区块&nbsp;&lt;&lt;------&nbsp;src中10区块(2)</p><p>注意:这里的第3操作数所代表的dest寄存器区块顺序是3210,而不是0123,x通常存放在0区块,内存中地址越小的就放在编号越小的区块~~总之别给搞反了就好,这里的确很容易混淆。</p><p>而这里其实有一个万恶的限制!!!:如果dest和src是不同的寄存器,那么3、4区块的值会取dest自己区块中的,而不是src区块中的值。所以,上面执行过的dest实际是这样的:<br/>dest&nbsp;=&nbsp;&nbsp;y1,&nbsp;w1,&nbsp;x2,&nbsp;z2</p><p>dest第3区块&nbsp;&lt;&lt;------&nbsp;dest中01区块(1)<br/>dest第2区块&nbsp;&lt;&lt;------&nbsp;dest中11区块(3)<br/>dest第1区块&nbsp;&lt;&lt;------&nbsp;src中00区块(0)<br/>dest第0区块&nbsp;&lt;&lt;------&nbsp;src中10区块(2)</p><p>如果还没明白的话.....&nbsp;&nbsp;可以发论坛中的短消息给我。</p><p><br/>SSE基本指令的计算有“加减乘除”四则运算,分别是:&nbsp;addps,&nbsp;subps,&nbsp;mulps,&nbsp;divps。很好记的,都是x86基本指令后面加个ps后缀。如果是ss后缀,则代表只有第0个区间做运算,速度会比ps的快20%左右,在AMD的CPU上会比浮点指令要慢,所以,传说中的性能提升400%就是纯属口胡,因为那只是理论值。但是300%还是可以做到的,特别是复杂的算法就更容易做到,比如说矩阵相乘。</p><p>现在发一段矩阵相乘的代码,出于是重点介绍SSE指令用法的缘故,主要注意了代码的可读性,所以没有对指令进行重排,所以会比我的最优化版慢25%左右的速度,但即便如此,已然是一般算法的3倍速了,看官们可以考回去实验一下,重排后的代码会在以后放出。</p><p>void&nbsp;MultMatrix(const&nbsp;Matrix*&nbsp;pOut,&nbsp;const&nbsp;Matrix*&nbsp;pIn1,&nbsp;const&nbsp;Matrix*&nbsp;pIn2)<br/>{<br/>if&nbsp;(!g_bUseSSE2)<br/>{<br/>//&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;&nbsp;&nbsp;xmm0&nbsp;&nbsp;&nbsp;*&nbsp;&nbsp;&nbsp;xmm4&nbsp;&nbsp;&nbsp;&nbsp;+&nbsp;&nbsp;&nbsp;&nbsp;xmm1&nbsp;&nbsp;&nbsp;*&nbsp;&nbsp;&nbsp;xmm5&nbsp;&nbsp;&nbsp;&nbsp;+&nbsp;&nbsp;&nbsp;&nbsp;xmm2&nbsp;&nbsp;*&nbsp;&nbsp;&nbsp;xmm6&nbsp;&nbsp;&nbsp;+&nbsp;&nbsp;&nbsp;&nbsp;xmm3&nbsp;&nbsp;&nbsp;*&nbsp;&nbsp;&nbsp;xmm7<br/>pOut-&gt;_11&nbsp;=&nbsp;pIn1-&gt;_11*pIn2._11&nbsp;+&nbsp;pIn1-&gt;_12*pIn2._21&nbsp;+&nbsp;pIn1-&gt;_13*pIn2._31&nbsp;+&nbsp;pIn1-&gt;_14*pIn2._41;<br/>pOut-&gt;_12&nbsp;=&nbsp;pIn1-&gt;_11*pIn2._12&nbsp;+&nbsp;pIn1-&gt;_12*pIn2._22&nbsp;+&nbsp;pIn1-&gt;_13*pIn2._32&nbsp;+&nbsp;pIn1-&gt;_14*pIn2._42;<br/>pOut-&gt;_13&nbsp;=&nbsp;pIn1-&gt;_11*pIn2._13&nbsp;+&nbsp;pIn1-&gt;_12*pIn2._23&nbsp;+&nbsp;pIn1-&gt;_13*pIn2._33&nbsp;+&nbsp;pIn1-&gt;_14*pIn2._43;<br/>pOut-&gt;_14&nbsp;=&nbsp;pIn1-&gt;_11*pIn2._14&nbsp;+&nbsp;pIn1-&gt;_12*pIn2._24&nbsp;+&nbsp;pIn1-&gt;_13*pIn2._34&nbsp;+&nbsp;pIn1-&gt;_14*pIn2._44;</p><p>pOut-&gt;_21&nbsp;=&nbsp;pIn1-&gt;_21*pIn2._11&nbsp;+&nbsp;pIn1-&gt;_22*pIn2._21&nbsp;+&nbsp;pIn1-&gt;_23*pIn2._31&nbsp;+&nbsp;pIn1-&gt;_24*pIn2._41;<br/>pOut-&gt;_22&nbsp;=&nbsp;pIn1-&gt;_21*pIn2._12&nbsp;+&nbsp;pIn1-&gt;_22*pIn2._22&nbsp;+&nbsp;pIn1-&gt;_23*pIn2._32&nbsp;+&nbsp;pIn1-&gt;_24*pIn2._42;<br/>pOut-&gt;_23&nbsp;=&nbsp;pIn1-&gt;_21*pIn2._13&nbsp;+&nbsp;pIn1-&gt;_22*pIn2._23&nbsp;+&nbsp;pIn1-&gt;_23*pIn2._33&nbsp;+&nbsp;pIn1-&gt;_24*pIn2._43;<br/>pOut-&gt;_24&nbsp;=&nbsp;pIn1-&gt;_21*pIn2._14&nbsp;+&nbsp;pIn1-&gt;_22*pIn2._24&nbsp;+&nbsp;pIn1-&gt;_23*pIn2._34&nbsp;+&nbsp;pIn1-&gt;_24*pIn2._44;</p><p>pOut-&gt;_31&nbsp;=&nbsp;pIn1-&gt;_31*pIn2._11&nbsp;+&nbsp;pIn1-&gt;_32*pIn2._21&nbsp;+&nbsp;pIn1-&gt;_33*pIn2._31&nbsp;+&nbsp;pIn1-&gt;_34*pIn2._41;<br/>pOut-&gt;_32&nbsp;=&nbsp;pIn1-&gt;_31*pIn2._12&nbsp;+&nbsp;pIn1-&gt;_32*pIn2._22&nbsp;+&nbsp;pIn1-&gt;_33*pIn2._32&nbsp;+&nbsp;pIn1-&gt;_34*pIn2._42;<br/>pOut-&gt;_33&nbsp;=&nbsp;pIn1-&gt;_31*pIn2._13&nbsp;+&nbsp;pIn1-&gt;_32*pIn2._23&nbsp;+&nbsp;pIn1-&gt;_33*pIn2._33&nbsp;+&nbsp;pIn1-&gt;_34*pIn2._43;<br/>pOut-&gt;_34&nbsp;=&nbsp;pIn1-&gt;_31*pIn2._14&nbsp;+&nbsp;pIn1-&gt;_32*pIn2._24&nbsp;+&nbsp;pIn1-&gt;_33*pIn2._34&nbsp;+&nbsp;pIn1-&gt;_34*pIn2._44;</p><p>pOut-&gt;_41&nbsp;=&nbsp;pIn1-&gt;_41*pIn2._11&nbsp;+&nbsp;pIn1-&gt;_42*pIn2._21&nbsp;+&nbsp;pIn1-&gt;_43*pIn2._31&nbsp;+&nbsp;pIn1-&gt;_44*pIn2._41;<br/>pOut-&gt;_42&nbsp;=&nbsp;pIn1-&gt;_41*pIn2._12&nbsp;+&nbsp;pIn1-&gt;_42*pIn2._22&nbsp;+&nbsp;pIn1-&gt;_43*pIn2._32&nbsp;+&nbsp;pIn1-&gt;_44*pIn2._42;<br/>pOut-&gt;_43&nbsp;=&nbsp;pIn1-&gt;_41*pIn2._13&nbsp;+&nbsp;pIn1-&gt;_42*pIn2._23&nbsp;+&nbsp;pIn1-&gt;_43*pIn2._33&nbsp;+&nbsp;pIn1-&gt;_44*pIn2._43;<br/>pOut-&gt;_44&nbsp;=&nbsp;pIn1-&gt;_41*pIn2._14&nbsp;+&nbsp;pIn1-&gt;_42*pIn2._24&nbsp;+&nbsp;pIn1-&gt;_43*pIn2._34&nbsp;+&nbsp;pIn1-&gt;_44*pIn2._44;<br/>}&nbsp;<br/>else<br/>{<br/>_asm<br/>{<br/>mov edx,&nbsp;pIn2; //&nbsp;这时保存的是pIn2<br/>movups xmm4,&nbsp;; //pIn2的第1行<br/>movups xmm5,&nbsp;; //pIn2的第2行<br/>movups xmm6,&nbsp;; //pIn2的第3行<br/>movups xmm7,&nbsp;; //pIn2的第4行</p><p>mov&nbsp;eax,&nbsp;pIn1; //&nbsp;这时保存的是pIn1<br/>mov edx,&nbsp;pOut;</p><p>mov ecx,&nbsp;4; //&nbsp;循环4次</p><p>LOOPIT: //&nbsp;开始循环<br/>movss xmm0,&nbsp;; xmm0&nbsp;=&nbsp;pIn1-&gt;x<br/>shufps xmm0,&nbsp;xmm0,&nbsp;0; 洗牌xmm0&nbsp;=&nbsp;pIn1-&gt;x,&nbsp;pIn1-&gt;x,&nbsp;pIn1-&gt;x,&nbsp;pIn1-&gt;x<br/>mulps xmm0,&nbsp;xmm4;</p><p>movss xmm1,&nbsp;; xmm1&nbsp;=&nbsp;pIn1-&gt;y<br/>shufps xmm1,&nbsp;xmm1,&nbsp;0; 洗牌xmm1&nbsp;=&nbsp;pIn1-&gt;y,&nbsp;pIn1-&gt;y,&nbsp;pIn1-&gt;y,&nbsp;pIn1-&gt;y<br/>mulps xmm1,&nbsp;xmm5;</p><p>movss xmm2,&nbsp;; xmm2&nbsp;=&nbsp;pIn1-&gt;z<br/>shufps xmm2,&nbsp;xmm2,&nbsp;0; 洗牌xmm2&nbsp;=&nbsp;pIn1-&gt;z,&nbsp;pIn1-&gt;z,&nbsp;pIn1-&gt;z,&nbsp;pIn1-&gt;z<br/>mulps xmm2,&nbsp;xmm6;</p><p>movss xmm3,&nbsp;; xmm3&nbsp;=&nbsp;pIn1-&gt;w<br/>shufps xmm3,&nbsp;xmm3,&nbsp;0; 洗牌xmm3&nbsp;=&nbsp;pIn1-&gt;w,&nbsp;pIn1-&gt;w,&nbsp;pIn1-&gt;w,&nbsp;pIn1-&gt;w<br/>mulps xmm3,&nbsp;xmm7;</p><p>addps xmm0,&nbsp;xmm1;<br/>addps xmm2,&nbsp;xmm3;<br/>addps xmm0,&nbsp;xmm2; 最终结果行保存在xmm0</p><p>movups ,&nbsp;xmm0; 将结果保存到pOut中<br/>add edx,&nbsp;16;<br/>add eax,&nbsp;16; 作为变址用</p><p>loop LOOPIT;<br/>}<br/>}<br/>}</p><p>这里的汇编对照上面的一般算法就很容易理解。下回我会说说矩阵类的关键算法。</p>
页: [1]
查看完整版本: 高效率的3D图形数学库(2)---SSE与矩阵相乘