上一次介绍了如何察看.net程序的asm代码,并且分析了System.Math下的部分函数。这一次,我们将更近一步,分析如何高效使用XNA中的数学库。
0.033秒的艺术 ---- XNA数学库中的陷阱
仅供个人学习使用,请勿转载,勿用于任何商业用途。
上一次介绍了如何察看.net程序的asm代码,并且分析了System.Math下的部分函数。这一次,我们将更近一步,分析如何高效使用XNA中的数学的库。下文仅以Matrix和Vector3为例,其余类型均可依此类推。为了达到测试和演示的目的,我写了一段相当”白痴”的代码:
test 1:
Code
AABB box = new AABB(new Vector3(34.4f, 4, 23));
Vector3 seed = box.Center;
public class AABB
{
private Vector3 center;
public Vector3 Center
{
get { return center; }
set { center = value; }
}
public AABB(Vector3 center)
{
this.center = center;
}
}
Test 1的目的很简单,只是为了检查JIT是否会inline类似vector3这样的简单属性。非常不幸,答案是否定的。
test 2:
Code
public static Vector3 Foo(Vector3 v, float radius)
{
Matrix a;
Matrix.CreateTranslation(ref v, out a);
Matrix b = Matrix.CreateTranslation(v);
Matrix.Multiply(ref a, ref b, out a);
Vector3 f1 = b.Forward;
Vector3 f2;
f2.X = b.M31;
f2.Y = b.M32;
f2.Z = b.M33;
Vector3 f3;
Vector3.Add(ref f1, ref f2, out f3);
Vector3.Transform(ref f3, ref a, out f1);
return f1;
}
Test 2才是本文的重点,为了方便讨论,把Test2的代码分为几段:
section 1:
Code
public static Vector3 Foo(Vector3 v, float radius)
{
Matrix a;
Matrix.CreateTranslation(ref v, out a);
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 sub esp,0F4h
0000000b mov esi,ecx
0000000d lea edi,[ebp+FFFFFF54h]
00000013 mov ecx,29h //29h = 41
00000018 xor eax,eax //eax = 0
0000001a rep stos dword ptr es:[edi] // for(i<41){new float = 0}
0000001c mov ecx,esi
0000001e mov dword ptr [ebp+FFFFFF04h],ecx
00000024 cmp dword ptr ds:[03A97E8Ch],0
0000002b je 00000032
0000002d call 76CFD6C9 //throw exception here???
00000032 lea ecx,[ebp+0Ch] //pass arg
00000035 lea edx,[ebp-48h] //pass arg
00000038 call dword ptr ds:[00F46FB8h] //call CreateTranslation
这一部分的代码一开始让我迷惑了很久,因为实际与调用CreateTranslation相关的指令只有032-038三条,前面那堆代码是干什么的呢,难道是编译器出错了?原以为有了asm就不用看IL了,可惜直到我重现查看了IL,才恍然大悟:怎么把函数初始化局部堆栈的部份忘了呢。Foo中将用到5个临时变量:2个Matrix,3个Vector3。最有趣的是。CIL没有初始化5个不同的struct,而是初始化了41个独立的float值!018~01a完成了初始化的工作,把41个float初始化为0。至于000~00d的部份,并不是太重要,可以忽略它们。奇怪的是01c~024的部份,我始终没看明白这段代码的目的,猜测应该是在做安全性检查,比如是否有stackover flow,如果有则跳转到位于76CFD6C9C处的函数。
结论是:函数中临时变量的个数会影响函数效率,01a表示重复41次以初始化所有变量。
section 2:
Code
Matrix b = Matrix.CreateTranslation(v);
0000003e lea eax,[ebp+0Ch]
00000041 push dword ptr [eax+8]
00000044 sub esp,8
00000047 movq xmm0,mmword ptr [eax]
0000004b movq mmword ptr [esp],xmm0
00000050 lea ecx,[ebp+FFFFFF14h]
00000056 call dword ptr ds:[00F46FACh]
0000005c lea edi,[ebp+FFFFFF78h] //address of b
00000062 lea esi,[ebp+FFFFFF14h] //address of returen value
00000068 mov ecx,10h //10h = 16
0000006d rep movs dword ptr es:[edi],dword ptr [esi]//copy return value to b
显然,同样的CreateTranslation,这里要比前面复杂很多。我不是汇编专家,所以041~050究竟干了什么,我一头雾水(希望有高人指点)。至于05c~06d的部份,则是CreateTranslation把返回值复制给b,注意,这里进行了16个float值的复制。然而并不十分明显的是这次所调用的CreateTranslation的地址和上一次完全不同。如果你有兴趣查看CreateTranslation(Vector3)的asm,会发现它比CreateTranslation(ref Vector3, out Matrix)多做了2件事:1,创建16个float大小的局部堆栈;2,在计算完成时,把局部堆栈的值复制到了一块临时内存,同样是16次mov。
结论是:非ref版本的函数比ref版本多执行了48条指令!对于value type来说,仅仅是传递方式,就会带来巨大性能差异。
section 3:
Code
Matrix.Multiply(ref a, ref b, out a);
0000006f lea eax,[ebp-48h]
00000072 push eax
00000073 lea ecx,[ebp-48h]
00000076 lea edx,[ebp+FFFFFF78h]
0000007c call dword ptr ds:[00F472A8h]
这里再次验证了之前的结论,对于ref版本的函数来说,只需直接传递参数地址,调用函数。
section 4:
Code
part 1:
Vector3 f1 = b.Forward;
00000082 lea ecx,[ebp+FFFFFF78h]
00000088 lea edx,[ebp+FFFFFF08h]
0000008e call dword ptr ds:[00F46F28h]
00000094 lea edi,[ebp+FFFFFF6Ch]
0000009a lea esi,[ebp+FFFFFF08h]
000000a0 movq xmm0,mmword ptr [esi]
000000a4 movq mmword ptr [edi],xmm0
000000a8 add esi,8
000000ab add edi,8
000000ae movs dword ptr es:[edi],dword ptr [esi]
part 2:
Vector3 f2;
f2.X = b.M31;
000000af fld dword ptr [ebp-68h]
000000b2 fstp dword ptr [ebp+FFFFFF60h]
f2.Y = b.M32;
000000b8 fld dword ptr [ebp-64h]
000000bb fstp dword ptr [ebp+FFFFFF64h]
f2.Z = b.M33;
000000c1 fld dword ptr [ebp-60h]
000000c4 fstp dword ptr [ebp+FFFFFF68h]
这里的两小段代码完成了同一件事。第一个版本使用了直接使用了Matrix的属性,第二个版本则手动访问基本元素。Forward同样没有内联,不过Forward与Test1所讨论的Center属性还不太一样,Center相对要简单许多——直接返回一个存在的值,而Forward则需要重新”组合”一个值返回,所以没有inline勉强可以接受。让人惊讶的是访问这样一个属性居然用了10条指令,如果再算上08e处所调用的函数,那么这个数字还要加上14,一共28条!而我们手动内联的代码只用了6条指令,big win。
结论是:合理应用手动内联提高性能。
section 5:这部份则没有太多的意义,只是为了让之前的代码不被编译器优化而存在,略
最后,我想各位对如何正确使用XNA中的数学库,以及进行优化有大概了解了吧。多研究asm代码,你一定会有更多发现:)