C#脚本引擎 CS-Script 之(二)——性能评测

以下以一个简单的HelloWord程序为例,来分析csscript脚本引擎的性能。

1 class HelloWorld
3 {
4     public void SayHello()
5     {
6         Console.WriteLine("Hello World, from internal!");
7     }
8 }

 

一、测试环境

运行的机器硬件配置:Intel Dore Duo CPU,内存 4

开发环境: vs2010

二、使用程序内部类和使用脚本的性能比较

 1  static void Main(string[] args)
 2         {
 3             CallFromInternal();
 4             CallFromScript();
 5         }
 6 
 7         static void CallFromInternal()
 8         {
 9             Console.WriteLine("");
10             Console.WriteLine("CallFromInternal");
11             DateTime beginTime = DateTime.Now;
12 
13             HelloWorld hello = new HelloWorld();
14             TimeSpan span = DateTime.Now - beginTime;
15             Console.WriteLine("create instance timespan: {0}", span);
16             beginTime = DateTime.Now;
17             hello.SayHello();
18 
19             span = DateTime.Now - beginTime;
20             Console.WriteLine("call helloWorld timespan: {0}", span);
21         }
22 
23 
24         static void CallFromScript()
25         {
26             Console.WriteLine("");
27             Console.WriteLine("CallFromScript");
28             DateTime beginTime = DateTime.Now;
29             
30             dynamic hello = CSScript.Evaluator.LoadFile("HelloWorld.cs");
31             TimeSpan span = DateTime.Now - beginTime;
32             Console.WriteLine("load and precompile script file, timespan= {0}", span);
33 
34             beginTime = DateTime.Now;
35             hello.SayHello();
36 
37             span = DateTime.Now - beginTime;
38             Console.WriteLine("call helloWorld timespan: {0}", span);
39         }

从以上两个函数的输出结果来看,直接调用程序内部函数的时间大概是2ms,而通过脚本引擎来同样一个HelloWorld的时间就达到了835ms,时间差距有400倍

835ms中,动态编译及其对象创建就花了814ms,而函数调用则21ms,所以即使抛开动态编译的成本,这个函数调用,由于内部其实是使用反射的机制来实现的,所以性能损失也比较明显。

三、一次动态编译多次调用

测试代码:

 1 static void CallFromSameScriptLoad1TimeAndCall4Times()
 2         {
 3             Console.WriteLine("");
 4             Console.WriteLine("CallFromSameScriptLoad1TimeAndCall4Times");
 5             DateTime beginTime = DateTime.Now;
 6             TimeSpan span;
 7             dynamic hello = CSScript.Evaluator.LoadFile("HelloWorld.cs");
 8             span = DateTime.Now - beginTime;
 9             Console.WriteLine("load and precompile script file, timespan= {0}", span);
10             
11             for (int i = 0; i < 4; ++i)
12             {
13                 beginTime = DateTime.Now;
14                 hello.SayHello();
15                 span = DateTime.Now - beginTime;
16                 Console.WriteLine("call helloWorld {0} time, timespan: {1}", i+1, span);
17                 Console.WriteLine("");
18             }
19         }

 

    行结果如下, 可以看出,第一次调用花了21ms,后面3次调用的时间基本可以忽略。那么推测,第一次是因为需要通过反射的方式找到SayHello方法的引用,后面的几次调用估计已经把该方法的引用缓存了,就可以直接前面查找好的委托,少了一个通过反射查找的过程,所以速度基本和调用本地方法相当。
以上只是推测,后续需要查阅源码分析看看。

 


四、多次动态编译同一个脚本并调用方法的性能分析

测试代码:

 1  static void CallFromSameScriptLoadAndCall4Times()
 2         {
 3             Console.WriteLine("");
 4             Console.WriteLine("CallFromSameScriptLoadAndCall4Times");
 5 
 6             TimeSpan span;
 7             for (int i = 0; i < 4; ++i)
 8             {
 9                 DateTime beginTime = DateTime.Now;       
10                 dynamic hello = CSScript.Evaluator.LoadFile("HelloWorld.cs");
11                 span = DateTime.Now - beginTime;
12                 Console.WriteLine("load and precompile script file, {0}, timespan= {1}", i+1, span);
13                 beginTime = DateTime.Now;
14                 hello.SayHello();
15 16 
17                 span = DateTime.Now - beginTime;
18                 Console.WriteLine("call helloWorld {0} time, timespan: {1}", i+1, span);
19                 Console.WriteLine("");
20             }            
21         }

 

测试结果如下,第一次调用的时间花销大,后续的时间花销基本相当于上一节中的第一次调用方法的时间。

那么推测:

(1) 对于同一个cs源文件,第一次编译之后会缓存,会把程序集缓存到内存中,后续再调用LoadFile的时候,实际上加载的是内存中缓存的程序集;

(2)第二次及其后续调用LoadFile("HelloWorld.cs"),实际上都是使用内存中的程序集,但是通过反射的方式找到HelloWorld类还是要每次去做的,所以一般还需要花费3ms左右;

(3)然后由于每个循环中都重新创建了一个新的HelloWord对象,在每个循环中去调用hello.SayHello();的时候,实际上还是实时的使用反射机制去查找hello对象中的SayHello方法,所以这里的时间花销省不了,一般也需要花费7ms左右。

 

推测:

(1)同一个程序集,多次编译,会使用第一次编译缓存的程序集;

(2)同一个类的多个对象的同一个方法(比如HelloWord类的SayHello方法),每个对象第一次调用该方法时,都需要使用反射方式去查找,所以此时性能较低;

五、动态编译多个不同的脚本的性能分析

测试代码:

 1 static void CallFromMultiScriptLoadAndCall4Times()
 2         {
 3             Console.WriteLine("");
 4             Console.WriteLine("CallFromMultiScriptLoadAndCall4Times");
 5 
 6             TimeSpan span;
 7             for (int i = 0; i < 4; ++i)
 8             {
 9                 DateTime beginTime = DateTime.Now;
10                 string fileName = string.Format("HelloWorld{0}.cs", i + 1);
11                 dynamic hello = CSScript.Evaluator.LoadFile(fileName);
12                 span = DateTime.Now - beginTime;
13                 Console.WriteLine("load and precompile script file{2}, {0}, timespan= {1}", i+1, span, fileName);
14                 beginTime = DateTime.Now;
15                 hello.SayHello();
16                 span = DateTime.Now - beginTime;
17                 Console.WriteLine("call helloWorld {0} time, timespan: {1}", i+1, span);
18             }
19         }

 

测试结果如下:

 这里分别动态编译了四个源文件,并调用对应的方法。但是只有第一次编译的时候速度慢,后续三次动态编译的速度和上一节动态编译同一个源文件的速度一样快。到这里就推翻了上一节的结论,说明上一节中2~4次的动态编译速度快,不是因为缓存了第一次动态编译的程序集。那么推测可能是因为第一次要动态编译的时候,程序要将.NET的用于动态编译的程序集(CSharpCodeProvider)加载到内存中,这个过程可能比较花时间,而动态编译本身是很快的。

六、结论

(1)在使用cs-script脚本引擎的时候,该程序第一次做动态编译时,需要有个1s左右的初始化时间;

(2)对于脚本中类的对象的方法的调用,在第一次调用某个对象的方法时(比如上文的HelloWorld类的hell对象的SayHello()方法),由于要使用反射方式去查找该犯法的委托,所以相比原生的对象方法调用要多10ms左右,后续的调用则和原生的方法差不多。

(3)cs-script编译一个普通源文件的时间基本是毫秒级别,一般在10ms以内,对于一般脚本数量不是很多(比如几十个)的情况,一般也就是多花几百毫秒,基本可以忽略;

综上,在引入了cs-script脚本引擎之后,在享受了脚本所带来的动态特性的同时,只是在初始化的时候需要多花1s左右的时间,其他情况的性能损失基本可以忽略。

 七、相关源码

CSScript系列之(二)——性能评测.zip

 

 

本系列包括:

C#脚本引擎 CS-Script 之(一)——初识 

C#脚本引擎 CS-Script 之(二)——性能评测 

C#脚本引擎CS-Script之(三)——如何部署 

posted @ 2015-08-05 19:45 拿走不谢 阅读(...) 评论(...) 编辑 收藏