0.033秒的艺术 --- the Cost of System.Math
仅供个人学习使用,请勿转载,勿用于任何商业用途。
在对.net程序进行调试或者性能测试时,常常需要查看生成的IL代码,但仅仅有IL代码还是不够的,有时我们还希望查看CLR生成的最终asm代码。在VS里,可以非常方便的查看最终的asm代码:当程序执行到断点时,在代码窗口右键选择Go To Disassemble就可以。但是,当通过VS Debug程序时,为了方便调试,CLR通常不会生成最优化的代码。所以为了得到实际运行时的asm代码,还必须做以下设置:
1,在Release模式下编译代码;
2. 打开工程属性窗口,选择”Build”页面--- “Advanced”,弹出窗口的“Debug Info”项设置为”pdb-only”。
3. 打开Tools => Options => Debugging => General,保证Suppress JIT optimization on module load和Enable Just My Code处于未选中状态。
可以在这里找到关于配置的更多资料。
我对.net 数学库做了一系列测试,结果可谓喜忧参半。当然,不同机器上可能得到不同的asm代码,以下是我的测试配置:intel Q6600 + .net framework 3.5 sp1。下面就来看看System.Math下个函数的性能。测试代码如下:
array[i] = Math.XXX(array[i]);
1. Math.Sqrt, Math.Sin, Math.Cos是所能期望的最理想实现,三个函数分别直接映射为fsqrt, fsin和fcos三条浮点汇编指令,可以认为这3个函数与汇编代码的效率一样。
Code
array[i] = Math.Sqrt(array[i]);
00000092 fild dword ptr [eax+edx*4+8]
00000096 fsqrt
00000098 fstp qword ptr [ebx+edx*8+8]
array[i] = Math.Sin(array[i]);
00000092 fild dword ptr [eax+edx*4+8]
00000096 fsin
00000098 fstp qword ptr [ebx+edx*8+8]
2. Math.Asin, Math.Acos, Math.Tan, Math.Atan, Math.floor, Math.Cell, Math.Log,Math.Exp,Math.Floor,Math.Pow,Math.Round以及其他所有以h结尾的三角函数:在Disassemble下,只能看到这些函数并没有inline。下面是Math.Tan函数的disassemble代码,在VS里是无法访问7935A4AB处的代码(实际上这个地址也根本不正确,这是vs里一个邪恶的bug):
Code
double tan = Math.Tan(c3.X);
00000219 fld dword ptr [ebp-30h]
0000021c sub esp,8
0000021f fstp qword ptr [esp]
00000222 call 7935A4AB
00000227 fstp qword ptr [ebp-38h]
查看这几个函数的IL代码,可以发现它们都被标记为” cil managed internalcall”。MS所有文档中对这个标记的解释都非常少,实际上它们将调用一些内部的非托管代码。在SOS的帮助下,可以发现Math.Tan的实际地址位于7A2C37FB,相应的代码则是:
Code
Unmanaged code
7A2C37FB 55 push ebp
7A2C37FC 8BEC mov ebp,esp
7A2C37FE DD4508 fld qword ptr [ebp+8]
7A2C3801 D9F2 fptan
7A2C3803 DDD8 fstp st(0)
7A2C3805 5D pop ebp
7A2C3806 C20800 ret 8
7A2C3809 55 push ebp
7A2C380A 8BEC mov ebp,esp
7A2C380C DD4508 fld qword ptr [ebp+8]
对于Math.Tan,最终仍然生成了fptan这样的cpu指令,但为什么和sin的差别会那么大呢,确实比较奇怪。至于另外剩下的函数,虽然算法不同,但实现的手段都是类似的,都是用非托管代码所写,并且可能导致多次对其他内部函数的调用。感兴趣可以用sos逐个查看。
3. Math.Abs. 这个函数比较特别,参数类型不同,所生成的代码也不同。对于浮点数来说,将会直接映射为浮点汇编指令fabs。对于int,却出乎意料的复杂。代码会想检查参数是否为负数,如果是,则需要进一步调用函数System.Math.AbsHelper,这是Math类的一个私有方法,它会进一步检查数据是否会溢出。如果不考虑安全性,自己编写一个简单的整数绝对值表达式要高效很多:
Code
//asm code for Math.Abs(float/double)
00000086 fld qword ptr [ebx+edx*8+8]
0000008a fabs
0000008c fstp qword ptr [ebx+edx*8+8]
//asm code for Math.Abs(integer)
0000008e mov ecx,dword ptr [eax+esi*4+8]
00000092 test ecx,ecx
00000094 jl 0000009A
00000096 mov eax,ecx
00000098 jmp 0000009F
0000009a call 75F1E728 //call Math.AbsHelper if integer is a negative number
0000009f mov dword ptr [ebp-24h],eax
000000a2 fild dword ptr [ebp-24h]
000000a5 cmp esi,dword ptr [ebx+4]
000000a8 jae 00000286
000000ae fstp qword ptr [ebx+esi*8+8]
//asm code for Math.AbsHelper
00000000 push ebp
00000001 mov ebp,esp
00000003 push eax
00000004 cmp ecx,80000000h
0000000a je 00725A00
00000010 neg ecx
00000012 mov eax,ecx
00000014 mov esp,ebp
00000016 pop ebp
00000017 ret //return if no overflow occurred
00000018 mov ecx,7994E990h
0000001d call FF83A548
00000022 mov dword ptr [ebp-4],eax
00000025 mov edx,790C1000h
0000002a mov ecx,70005A82h
0000002f call FF83A598
00000034 mov ecx,eax
00000036 call FF8641C8
0000003b mov edx,eax
0000003d mov ecx,dword ptr [ebp-4]
00000040 call FFDCD44C
00000045 mov ecx,dword ptr [ebp-4]
00000048 call FF83A5B0
0000004d int 3
//optimized unsafe abs:
static public int FastAbs(int a)
{
return (a >= 0) ? a : -a;
}
//asm code for unsafe abs which is inlined
00000091 mov eax,dword ptr [ebx+ecx*4+8]
00000095 test eax,eax
00000097 jge 0000009D
00000099 neg eax
0000009b jmp 0000009D
0000009d cmp ecx,edx
0000009f jae 00000296
000000a5 mov dword ptr [ebx+ecx*4+8],eax
4. Math.Max, Math.Min则是用普通托管语言编写的代码,没有特别优化,但这2个方法是inline的。