通过IL Emit来创建类型的探究

  最近刚开始研究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。

原文地址:https://www.cnblogs.com/zzyDS/p/12165775.html