0.033秒的艺术 ---- XNA数学库中的陷阱
仅供个人学习使用,请勿转载,勿用于任何商业用途。
上一次介绍了如何察看.net程序的asm代码,并且分析了System.Math下的部分函数。这一次,我们将更近一步,分析如何高效使用XNA中的数学的库。下文仅以Matrix和Vector3为例,其余类型均可依此类推。为了达到测试和演示的目的,我写了一段相当”白痴”的代码:
test 1:
Test 1的目的很简单,只是为了检查JIT是否会inline类似vector3这样的简单属性。非常不幸,答案是否定的。
test 2:
Test 2才是今天的主角,为了方便讨论,把Test2的代码分为几段:
section 1:
这一部分的代码一开始让我迷惑了很久,因为实际与调用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:
显然,同样的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:
这里再次验证了之前的结论,对于ref版本的函数来说,只需直接传递参数地址,调用函数。
section 4:
这里的两小段代码完成了同一件事。第一个版本使用了直接使用了Matrix的属性,第二个版本则手动访问基本元素。Forward同样没有内联,不过Forward与Test1所讨论的Center属性还不太一样,Center相对要简单许多——直接返回一个存在的值,而Forward则需要重现”组合”一个值作为返回值,所以没有inline勉强可以接受。让人惊讶的是访问这样一个属性居然用了10条指令,如果再算上08e处所调用的函数,那么这个数字还要加上14,一共28条!而我们手动内联的代码只用了6条指令,big win。
结论是:合理应用手动内联提高性能。
section 5:
这部份则没有太多的意义,只是为了让之前的代码不被编译器优化而存在,不再继续讨论。
最后,我想各位对如何正确使用XNA中的数学库,以及进行优化有大概了解了吧。多研究asm代码,你一定会有更多发现:)