高效率的3D图形数学库(1) ----Vector概览
潜水了很长时间,该是做点贡献的时候了,最近写的,发上来给各位拍砖: <p></p><p> 最近研究汇编比较多,看自己C++代码的汇编源码简直是一种折磨,这迫使我将所有数学库重新用汇编指令实现,当然,包括对CPUID的检测和使用扩展指令集。测试结果是与D3DX9的数学函数比较的,效果另人满意,除了矩阵相乘的算法总是与D3DXMatrixMultiply函数有7%的差距外,其余都是持平甚至遥遥领先(也许是我疯了,有新的看官可以自己测一下)。 由于本人技术浅薄,测试效率的方法又比较简陋,所以还请高手指正!<br/>第一步是介绍我的Vector类,以下是声明:</p><p>struct __declspec(dllexport) Vector <br/>{</p><p>/******************变量********************/</p><p>float x, y, z, w;</p><p>/******************构造*******************/</p><p>// 构造函数<br/>Vector() {}<br/>// 构造函数<br/>Vector(const float* v);<br/>// 构造函数<br/>Vector(float _x, float _y, float _z, float _w);</p><p>/******************方法*******************/</p><p>// 设置向量<br/>void SetVector(const float* v);<br/>// 设置向量<br/>void SetVector(float _x, float _y, float _z, float _w);<br/>// 减法<br/>void Difference(const Vector* pSrc, const Vector* pDest);<br/>// 反向量<br/>void Inverse();<br/>// 单位化向量<br/>void Normalize();<br/>// 是否单位向量<br/>bool IsNormalized();<br/>// 向量长度(慢)<br/>float GetLength();<br/>// 向量长度的平方(快)<br/>float GetLengthSq();<br/>// 通过两向量求叉乘,结果保存在该向量中<br/>void Cross(const Vector* pU, const Vector* pV);<br/>// 求两向量夹角<br/>float AngleWith(Vector& v);</p><p>/*************运算符重载*****************/</p><p>// 运算符重载<br/>void operator += (Vector& v);<br/>// 运算符重载<br/>void operator -= (Vector& v);<br/>// 运算符重载<br/>void operator *= (float v);<br/>// 运算符重载<br/>void operator /= (float v);<br/>// 运算符重载<br/>Vector operator + (Vector& v) const;<br/>// 运算符重载<br/>Vector operator - (Vector& v) const;<br/>// 运算符重载<br/>float operator * (Vector& v) const;<br/>// 运算符重载<br/>void operator *= (GaiaMatrix& m);<br/>// 运算符重载<br/>Vector operator * (float f) const;<br/>// 运算符重载<br/>bool operator ==(Vector& v);<br/>// 运算符重载<br/>bool operator !=(Vector& v);<br/>// 运算符重载<br/>//void operator = (Vector& v);<br/>};</p><p>然后是简单的内联函数:</p><p>// 构造函数<br/>inline Vector::Vector(const float* v)<br/>: x(v)<br/>, y(v)<br/>, z(v)<br/>, w(v)<br/>{<br/>}</p><p>// 构造函数<br/>inline Vector::Vector(float _x, float _y, float _z, float _w)<br/>: x(_x)<br/>, y(_y)<br/>, z(_z)<br/>, w(_w)<br/>{<br/>}</p><p>// 设置向量<br/>inline void Vector::SetVector(const float* v)<br/>{<br/>x = v; y = v; z = v;<br/>}</p><p>// 设置向量<br/>inline void Vector::SetVector(float _x, float _y, float _z, float _w)<br/>{<br/>x = _x; y = _y; z = _z; w = _w;<br/>}</p><p>// 减法<br/>inline void Vector::Difference(const Vector* pSrc, const Vector* pDest)<br/>{<br/>x = pDest->x - pSrc->x;<br/>y = pDest->y - pSrc->y;<br/>x = pDest->z - pSrc->z;<br/>}</p><p>// 反向量<br/>inline void Vector::Inverse()<br/>{<br/>x = -x; y = -y; z = -z;<br/>}</p><p>// 是否单位向量<br/>inline bool Vector::IsNormalized()<br/>{<br/>return CmpFloatSame(x*x+y*y+z*z, 1.0f);<br/>}</p><p>// 运算符重载<br/>inline void Vector::operator += (Vector& v)<br/>{<br/>x += v.x; y += v.y; z += v.z;<br/>}<br/>// 运算符重载<br/>inline void Vector::operator -= (Vector& v)<br/>{<br/>x -= v.x; y -= v.y; z -= v.z;<br/>}<br/>// 运算符重载<br/>inline void Vector::operator *= (float f)<br/>{<br/>x *= f; y *= f; z *= f;<br/>}<br/>// 运算符重载<br/>inline void Vector::operator /= (float f)<br/>{<br/>f = 1.0f/f;<br/>x *= f; y *= f; z *= f;<br/>}<br/>// 运算符重载<br/>inline Vector Vector::operator + (Vector& v) const<br/>{<br/>return Vector(x+v.x, y+v.y, z+v.z, w);<br/>}<br/>// 运算符重载<br/>inline Vector Vector::operator - (Vector& v) const<br/>{<br/>return Vector(x-v.x, y-v.y, z-v.z, w);<br/>}<br/>// 运算符重载<br/>inline float Vector::operator * (Vector& v) const<br/>{<br/>return (x*v.x + y*v.y + z*v.z);<br/>}<br/>// 运算符重载<br/>inline Vector Vector::operator * (float f) const<br/>{<br/>return Vector(x*f, y*f, z*f, w);<br/>}<br/>// 运算符重载<br/>inline bool Vector::operator ==(Vector& v)<br/>{<br/>return ((((x-v.x)<FLOAT_EPS && (x-v.x)>-FLOAT_EPS) || ((y-v.y)<FLOAT_EPS && (y-v.y)>-FLOAT_EPS) || ((z-v.z)<FLOAT_EPS && (z-v.z)>-FLOAT_EPS))? false:true);<br/>}<br/>// 运算符重载<br/>inline bool Vector::operator !=(Vector& v)<br/>{<br/>return ((((x-v.x)<FLOAT_EPS && (x-v.x)>-FLOAT_EPS) || ((y-v.y)<FLOAT_EPS && (y-v.y)>-FLOAT_EPS) || ((z-v.z)<FLOAT_EPS && (z-v.z)>-FLOAT_EPS))? true:false);<br/>}</p><p>这里比较重要的优化有几点,也可以作为写代码的原则,非常非常重要:</p><p>1、可以用const的地方一定要用!编辑器会拿这个来优化的。<br/>2、return返回一个值的时候,如果可以的话,就一定要以构造函数的形式返回值。如:<br/>return Vector(x+v.x, y+v.y, z+v.z, w);<br/>3、多个数除以同一个数时,一定要按照如Vector::operator /= (float f)中的形式写。<br/>4、这样的小函数一定是要inline的!</p><p>以上4点一定要遵守,否则做出的汇编代码惨不忍睹!效率自然也是一落千丈,切记切记。</p><p>接下来是Vector的高级函数部分:</p><p>// 向量长度的平方(快)<br/>float Vector::GetLengthSq() // 潜在危险<br/>{<br/>_asm<br/>{<br/>fld dword ptr ;<br/>fmul dword ptr ;<br/>fld dword ptr ;<br/>fmul dword ptr ;<br/>faddp st(1),st;<br/>fld dword ptr ;<br/>fmul dword ptr ;<br/>faddp st(1),st ;<br/>}<br/>//return x*x + y*y + z*z;<br/>}</p><p>// 向量长度(慢)<br/>float Vector::GetLength()<br/>{<br/>float f;<br/>if (g_bUseSSE2)<br/>{<br/>_asm<br/>{<br/>lea ecx, f;<br/>mov eax, this;<br/>mov dword ptr , 0; // w = 0.0f;</p><p>movups xmm0, ;<br/>mulps xmm0, xmm0;<br/>movaps xmm1, xmm0;<br/>shufps xmm1, xmm1, 4Eh; 洗牌<br/>addps xmm0, xmm1;<br/>movaps xmm1, xmm0;<br/>shufps xmm1, xmm1, 11h; 洗牌<br/>addss xmm0, xmm1;</p><p>sqrtss xmm0, xmm0; 第一个单元求开方<br/>movss dword ptr , xmm0; 第一个单元的值给ecx指向的内存空间</p><p>mov dword ptr , 3F800000h; // 3F800000h == 1.0f<br/>}<br/>}<br/>else<br/>{<br/>f = (float)sqrt(x*x+y*y+z*z);<br/>}<br/>return f;<br/>}</p><p>// 单位化向量<br/>void Vector::Normalize()<br/>{<br/>if (g_bUseSSE2)<br/>{<br/>_asm<br/>{<br/>mov eax, this;<br/>mov dword ptr, 0;</p><p>movups xmm0, ;<br/>movaps xmm2, xmm0;<br/>mulps xmm0, xmm0;<br/>movaps xmm1, xmm0;<br/>shufps xmm1, xmm1, 4Eh;<br/>addps xmm0, xmm1;<br/>movaps xmm1, xmm0;<br/>shufps xmm1, xmm1, 11h;<br/>addps xmm0, xmm1;</p><p>rsqrtps xmm0, xmm0;<br/>mulps xmm2, xmm0;<br/>movups , xmm2;</p><p>mov dword ptr , 3F800000h;<br/>}<br/>}<br/>else<br/>{<br/>float f = (float)sqrt(x*x+y*y+z*z);<br/>if (f != 0.0f)<br/>{<br/>f = 1.0f/f;<br/>x*=f; y*=f; z*=f;<br/>}<br/>}<br/>}</p><p>// 通过两向量求叉乘,结果保存在该向量中<br/>void Vector::Cross(const Vector* pU, const Vector* pV)<br/>{<br/>if (g_bUseSSE2)<br/>{<br/>_asm<br/>{<br/>mov eax, pU;<br/>mov edx, pV;</p><p>movups xmm0, <br/>movups xmm1, <br/>movaps xmm2, xmm0<br/>movaps xmm3, xmm1</p><p>shufps xmm0, xmm0, 0xc9<br/>shufps xmm1, xmm1, 0xd2<br/>mulps xmm0, xmm1</p><p>shufps xmm2, xmm2, 0xd2<br/>shufps xmm3, xmm3, 0xc9<br/>mulps xmm2, xmm3</p><p>subps xmm0, xmm2</p><p>mov eax, this<br/>movups , xmm0</p><p>mov , 3F800000h;<br/>}<br/>}<br/>else<br/>{<br/>x = pU->y * pV->z - pU->z * pV->y;<br/>y = pU->z * pV->x - pU->x * pV->z;<br/>z = pU->x * pV->y - pU->y * pV->x;<br/>w = 1.0f;<br/>}<br/>}</p><p><br/>// 运算符重载<br/>void Vector::operator *= (Matrix& m) // 潜在危险<br/>{<br/>#ifdef _DEBUG<br/>assert(w!=1.0f && w!=0.0f);<br/>#endif</p><p>if (g_bUseSSE2)<br/>{<br/>_asm<br/>{<br/>mov ecx, this;<br/>mov edx, m;<br/>movss xmm0, ;<br/>//lea eax, vr;<br/>shufps xmm0, xmm0, 0; // xmm0 = x,x,x,x</p><p>movss xmm1, ;<br/>mulps xmm0, ;<br/>shufps xmm1, xmm1, 0; // xmm1 = y,y,y,y</p><p>movss xmm2, ;<br/>mulps xmm1, ;<br/>shufps xmm2, xmm2, 0; // xmm2 = z,z,z,z</p><p>movss xmm3, ;<br/>mulps xmm2, ;<br/>shufps xmm3, xmm3, 0; // xmm3 = w,w,w,w</p><p>addps xmm0, xmm1;<br/>mulps xmm3, ;</p><p>addps xmm0, xmm2;<br/>addps xmm0, xmm3; // xmm0 = result<br/>movups , xmm0;<br/>mov , 3F800000h;<br/>}</p><p>} <br/>else<br/>{<br/>Vector vr;<br/>vr.x = x*m._11 + y*m._21 + z*m._31 + w*m._41;<br/>vr.y = x*m._12 + y*m._22 + z*m._32 + w*m._42;<br/>vr.z = x*m._13 + y*m._23 + z*m._33 + w*m._43;<br/>vr.w = x*m._14 + y*m._24 + z*m._34 + w*m._44;</p><p>x = vr.x;<br/>y = vr.y;<br/>z = vr.z;<br/>w = 1.0f;<br/>}<br/>}</p><p><br/>// 求两向量夹角<br/>float Vector::AngleWith(Vector& v)<br/>{<br/>return (float)acosf((*this * v)/(this->GetLength()*v.GetLength()*2.0f));<br/>}</p><p>这里要说明3个函数:GetLengthSq,*= 和AngleWith<br/> GetLengthSq有潜在危险,因为我是根据.Net2003的编辑器来写的代码,我知道ecx==this,知道float的返回值是直接从浮点栈寄存器fstp到外面参数的,所以,我会用这种方法来写,甚至没有写返回值!而看此文的您可能不会使用与我一样的编辑器,所以,在理解了实质之后,运用合理的算法来实现你的数学库。后面的函数都使用了编辑器无关的方法写的。</p><p> *= 的运算符重载的潜在危险在于,Vector是4D的,可以表示3D的向量或者3D空间点坐标。如果是向量,则w==0,这样就只会受到旋转和缩放的影响。而如果是表示空间点,w==1,就会受到所有类型的变动,如平移、旋转和缩放。由于向量是不能平移的,处于对运算效率的考虑,这时候就需要数学库的调用者自己注意了。</p><p> AngleWith函数之所以不对其进行内联化,是因为在以后的文章中,我会去进一步优化这里的代码。GetLength和acosf都不是内联函数,我必须要将其展开,以汇编实现,并重新组织编码。这个函数好像在D3DX9的数学库中是没有的~~没办法比较了。</p><p>以上几个函数的效率与D3DX库比较结果大致是这样的:<br/> GetLengthSq微高于D3DX<br/> GetLength是D3DX速度的2倍多,因为D3D库没有用SSE指令。<br/> Normalize和Cross的速度比D3DX的高的太多,有些离谱。同样是因为D3D库没有用SSE指令。<br/> *=的效率低于D3DXVec3Transform约7%,有进一步提高的可能!高手来看看。D3DX库用的是3DNow!运算的,居然比SSE快!大概是因为我的AMD3000+的缘故吧...,换在Inter上应该速度差不多了。<br/> AngleWith没有办法评测,因为没有比照对象。</p><p> 很多算法都经过手工的指令重排,发现指令的顺序对效率的影响是非常大的!在改变指令顺序时一定要慎重!最好拷贝一份原来的,否则在排比较长的汇编代码时会把自己玩晕的~o~<br/> 顺便提几个很多人疑惑的问题:<br/> 1、那个C++库里的_mm_mov_ps()类似的代码,简直就是垃圾!想要效率就千万别用那个,好好的学习汇编,然后亲手写代码。那些库里的函数搞出的代码简直就是惨不忍睹!<br/> 2、movups和movaps的效率差距几乎可以忽略不计的!别为了快那么百分之一的速度就声明一个_m128的Vector或者Matrix,以后建立数组的时候可有你受的了!<br/> 3、本人的测试方法太菜了,就是循环1000万遍,用timeGetTime()看个大概。多运行几遍找个平均而已。所以,一旦Release模式的内联就测不出效率了~有时间的高人们可以去测试一下,估计能内联的函数都是快接近效率极限的,不太值得优化。<br/>如果对我的测试有什么疑惑,看官们可以考回去自己测试效率,换多种CPU试一下,我在这儿接受任何人的拍砖!</p><p> 下一次我将详细说说我对SSE和浮点指令的理解,以及最有用的矩阵相乘算法。<br/></p>
页:
[1]