最近刚开始研究IL,起源是看到Odin内部源码创建一个Type使用了这种做法,当时好奇为什么要这么做。
先丢出代码例子:
1 class TestClass 2 { 3 public TestClass() 4 { 5 mylist = new List<int>(); 6 for(int i=0;i<100;i++) 7 { 8 mylist.Add(i); 9 } 10 } 11 12 public int x = 5; 13 public List<int> mylist; 14 }
1 // Do Some Test 2 int needCnt = 10000; 3 var preTime = EditorApplication.timeSinceStartup; 4 for(int i=0;i<needCnt;i++) 5 { 6 TestClass t = new TestClass(); 7 } 8 var nowTime = EditorApplication.timeSinceStartup; 9 Debug.Log("通常创建的时间 " + (nowTime - preTime)); 10 11 var type = typeof(TestClass); 12 var constructor = type.GetConstructor(Type.EmptyTypes); 13 var method = new DynamicMethod(type.FullName + "_FastCreator", type, Type.EmptyTypes); 14 15 var il = method.GetILGenerator(); 16 17 il.Emit(OpCodes.Newobj, constructor); 18 il.Emit(OpCodes.Ret); 19 20 var fastCreator = (Func<TestClass>)method.CreateDelegate(typeof(Func<TestClass>)); 21 22 preTime = EditorApplication.timeSinceStartup; 23 for(int i=0;i<needCnt;i++) 24 { 25 var t = constructor.Invoke(new object[] {}); 26 } 27 nowTime = EditorApplication.timeSinceStartup; 28 Debug.Log("构造函数Invoke并且不转型创建的时间 " + (nowTime - preTime)); 29 30 preTime = EditorApplication.timeSinceStartup; 31 for(int i=0;i<needCnt;i++) 32 { 33 TestClass t = constructor.Invoke(new object[] {}) as TestClass; 34 } 35 nowTime = EditorApplication.timeSinceStartup; 36 Debug.Log("构造函数Invoke创建的时间 " + (nowTime - preTime)); 37 38 preTime = EditorApplication.timeSinceStartup; 39 for(int i=0;i<needCnt;i++) 40 { 41 TestClass t = fastCreator(); 42 } 43 nowTime = EditorApplication.timeSinceStartup; 44 Debug.Log("Emit创建的时间 " + (nowTime - preTime));
测试结果如下(直接在Unity上跑的):
看起来的结果就是,通过IL Emit的时间跟直接new一个的时间是同一个数量级别的,通过method.Invoke的时间要慢上一倍。
个人猜测Odin选择这种做法来创建种种类型的原因如下:1. 时间上比Invoke要快 2. 只有Type信息无法调用new(猜测?)3. 方便制造成一个委托,下次再用。
以后有更多心得了再补充。。。
---------------------------------------------------------------分割线----------------------------------------------------------------
又稍微了解了一下,第二点应该是错误的,可以通过 TestClass t = (TestClass)Activator.CreateInstance(type); 来创建对应的类型,甚至可以传入额外的参数来调用其他的构造函数。然后重新跑了下,新的测试结果如下:
可以看到的是,这种方法的时间开销也是蛮高的,不如IL Emit。
然后看了一下别人的博客,看到如下的一句话:
另外,凡事要分两面,Emit的效率高是有条件的,Emit动态创建方法的代价通常在1ms左右(一个非常高的初始代价),如果反射次数相当的少,原始反射的效率比Emit要高,只有当这个被Emit的方法被调用了相当多次(例如1000次)以后,才能补偿掉这个初始代价而lz的例子正好作为反面例子,每次创建一个动态方法(高代价),然后执行一行数据,然后就抛弃这个花了很大代价得到的动态方法,重新再创建...
大概理解了Odin为什么要采用这种做法,如上面所说的,把Emit动态创建的方法存在表里,下次可以直接调用,速度又快又避免了创建代价大的问题。Odin利用这种方法创建各种OdinDrawer。