背景
因工作需要,需要对java引入动态脚本的支持,当前可实现的动态脚本可选择的空间非常多,但是由于工作特性,作者需要满足一些特征(后面详述),于是把希望在网上看能否找到一些信息。网上针对脚本对比评测的文章有很多,包括涉及类似javascript、groovy、python、ruby等各种脚本,但是大部分为单一测试,且符合当前工作需求的评测较少。组合测试的又不太具有针对性,故作者结合工作场景有针对性的对部分动态脚本进行一期简单的性能评测。
需求分析
既然是特定工作业务场景,这里需要满足执行的脚本符合如下特征:
- 具有简单的逻辑判断能力:即动态脚本需要有,包括if语句,for循环等机制。这基本干掉了所有的表达式语言,包括优秀的google的aviator。当然如果仅仅只是简单的表达式解析,它是一个很好地选择
- 具有极高的安全稳定特性:这里不仅是需要被生产环境验证过的脚本引擎,更多的我需要脚本的功能只停留在数据处理及基本数据的构建上,不需要其他任何功能的实现,包括构建对象,各种IO及底层服务的调用。这基本干掉了类似groovy等经典的脚本语言,因为存在安全问题的同时,如何确保对内存的消耗有效回收又是另外不可忽略的风险(有很多使用了groovy出现OOM的案例)。
- 对性能的要求:虽然动态脚本本身都不是主推性能的,但是在生产环境,高并发是无法绕开的话题,能够在有限的条件下尽可能的满足高效性能也是重要考虑因素。
确定评测选手
针对实际场景及需求特性,经过从安全性,易用性等综合评估,最终选了3个具有代表性的选手进行对比评测:
选手1:最原生态
javascript
该动态脚本为java原生提供的能力,在「官方」「原生」等关键词的加持下,一直被认为有着非常优秀的性能条件,是不可忽略的对手
选手2:最轻量级
lua
业界普遍认为最轻量级的脚本语言。在「小」中做了最优的权衡,是所有实用性语言中规模最小的一种。因为它的小,被普遍用在移动端(含j2me)、游戏的动态脚本执行部分。同时又是因为它的快,又被普遍用在服务端领域(如nginx)中。
选手3:原生java
通过与原生java做对比比较,我们看看动态脚本与原生java到底有多大差距
评测备注
注意:每个脚本语言都有自身的优点和缺点,比如有的更贴近java语法,学习成本更低;有的附属设施更完善,应用场景更丰富;有的对资源消耗更少等。这些都不在本次的评测范围,本次仅仅只考虑对性能的一个对比,是在特定环境下的特定比较,不做整体好坏判断。
评测脚本内容
评测的脚本很简单,主要做这些事:
- 进行千万级的的for循环操作
- 进行不断的累加操作
- 进行简单的逻辑判断
- 进行字符串累加操作(部分场景)
最终看看各个脚本执行完成的时间,判断最终性能。
javascript脚本:
function test(){ var a=0; for(i=0; i<=10000000; i++){ if(a<i){ a++; }; }; return a}
lua脚本:
a = 0for i=0,10000000,1 do if(i > a) then a=a + 1 endendreturn a
纯java代码:
int a = 0;for(int i = 0;i<=10000000;i++){ if(i > a){ a++; }}
试验1
我们首先在1000w循环量级下跑下各个脚本情况,得到下表(单位ms)
javascript | lua | 纯java | |
第一次 | 647 | 1374 | 10 |
第二次 | 689 | 1360 | 10 |
第三次 | 685 | 1392 | 8 |
第四次 | 686 | 1474 | 9 |
第五次 | 702 | 1433 | 10 |
第六次 | 699 | 1430 | 10 |
第七次 | 690 | 1594 | 10 |
第八次 | 731 | 1419 | 10 |
第九次 | 698 | 1455 | 9 |
第十次 | 674 | 1473 | 10 |
平均 | 690 | 1440 | 9.6 |
可以看出javascript在速度上有较大的优势,基本是lua的两倍以上
试验2
实际操作中大部分时候还是会进去字符串操作,那这里再增加以下字符串累加操作试试:
javascript:c=c+'c'
lua:c=c .. 'c'
纯java:c+='c'
加上了字符串的处理以后,其他不变,进行测试,情况就大不一样了:
javascript |
lua |
纯java |
|
第一次 |
1829 |
x |
116 |
第二次 |
1854 |
x |
127 |
第三次 |
1841 |
x |
148 |
第四次 |
1866 |
x |
134 |
第五次 |
1839 |
x |
132 |
第六次 |
1788 |
x |
127 |
第七次 |
1824 |
x |
117 |
第八次 |
1871 |
x |
120 |
第九次 |
1821 |
x |
145 |
第十次 |
1839 |
x |
122 |
平均 |
1837 |
129 |
经过测试发现,即使纯java开销也不小。而此次lua直接超时(超过1min)
试验3
通过减少循环次数,最终在10w量级的for循环中跑出了结果
javascript |
lua |
纯java |
|
第一次 |
545 |
1268 |
6 |
第二次 |
611 |
1298 |
6 |
第三次 |
619 |
1252 |
7 |
第四次 |
566 |
1289 |
7 |
第五次 |
584 |
1309 |
7 |
第六次 |
564 |
1378 |
6 |
第七次 |
591 |
1336 |
6 |
第八次 |
616 |
1286 |
7 |
第九次 |
579 |
1259 |
6 |
第十次 |
583 |
1286 |
7 |
平均 |
585 |
1296 |
6.5 |
整体看仍然javascript具有较大的优势,是lua的近2倍
试验4
最后在实际生产中,我们往往还需要对脚本引擎进行初始化,这也需要消耗大量资源,我们将初始化次数放到一起进行测试看看效果怎么样:
for循环1w次,内部循环10次,不装配字符串。js代码如下(其他代码类似,故省略)
public static void main(String[] args) throws Exception { long now = System.currentTimeMillis(); for(int i = 0; i<10000;i++){ jsScript(); } System.out.println(System.currentTimeMillis() - now);} private static void jsScript() throws Exception { ScriptEngineManager mgr = new ScriptEngineManager(); ScriptEngine engine = mgr.getEngineByExtension("js"); engine.eval("function test(){var a=0;for(i=0;i<=10;i++){ if(a<i){a++;};}; return a}"); Invocable inv = (Invocable) engine; String value = String.valueOf(inv.invokeFunction("test")); }
js核心是构造这两个对象:
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine engine = mgr.getEngineByExtension("js");
lua是这个:
Globals globals = JsePlatform.standardGlobals();
纯java因为是宿主代码,不需要初始化
最终得到结果如下:
javascript |
lua |
纯java |
|
第一次 |
18758 |
1353 |
1 |
第二次 |
18948 |
1329 |
1 |
第三次 |
19214 |
1483 |
1 |
第四次 |
18781 |
1446 |
1 |
第五次 |
18815 |
1441 |
1 |
第六次 |
18929 |
1495 |
1 |
第七次 |
18782 |
1444 |
1 |
第八次 |
18860 |
1412 |
1 |
第九次 |
19046 |
1471 |
1 |
第十次 |
18353 |
1375 |
1 |
平均 |
18848 |
1425 |
1 |
结果大跌眼镜,加入初始化构造后,javascript反而比lua慢了不少,而且有近9倍的差距。
那是不是每次使用的时候都要初始化对象呢?,通过查看:
以及对应官网:
https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/api.html#BABHIFEF
发现js引擎不需要每次重复注册,只需要更新bindings即可。
ScriptContext newContext = new SimpleScriptContext(); newContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE); engine.setContext(newContext);
同理,lua也可以将这个单独拎出来
Globals globals = JsePlatform.standardGlobals();
试验5
javascript |
lua |
纯java |
|
第一次 |
3028 |
321 |
1 |
第二次 |
3477 |
308 |
1 |
第三次 |
3147 |
321 |
1 |
第四次 |
3160 |
321 |
1 |
第五次 |
3570 |
322 |
1 |
第六次 |
3331 |
314 |
1 |
第七次 |
3594 |
410 |
1 |
第八次 |
3431 |
318 |
1 |
第九次 |
3356 |
339 |
1 |
第十次 |
3573 |
340 |
1 |
平均 |
3367 |
331 |
1 |
这里javascript执行效率低于其他两个的原因主要是有个编译字节码的过程。
写在最后
▐ 结论
简单说说最终结论
- 不要偷懒。所有脚本引擎不要从头构建引擎对象,虽然这样简单粗暴。但是效率上也是有近5~6倍的差距
- 如果你的脚本相对比较复杂,里面有大量的for循环以及字符串处理。推荐使用javascript。它在处理复杂脚本优势很明显,当然这全靠他内部会编译成java字节码给到jvm执行的功劳(注意java6里的js不是同一个引擎,不会编译字节码,慢很多)。
- 如果你的脚本相对比较简单,没有大量的for循环等语句,那么lua是比较好的选择,占用资源更少,通用性更高。
- 无论是何种脚本语言,它的性能都是纯java的百分之一以上,除非必要,使用脚本语言一定要慎重。
▐ 参考文档
https://blog.csdn.net/fuhanghang/article/details/124723417https://blog.51cto.com/fengbohaishang/1080126https://www.iteye.com/topic/361794https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/api.html#BABHIFEFhttps://www.chrismoos.com/2010/03/24/groovy-scripts-and-jvm-security/https://stackoverflow.com/questions/30140103/should-i-use-a-separate-scriptengine-and-compiledscript-instances-per-each-threa
团队介绍
我们是大淘宝技术-行业与运营工作台团队,我们立足于对天猫淘宝行业商业价值理解,基于数字化驱动的商家运营、商品运营、内容运营策略,构建垂直行业消费者导购、交易、物流、服务创新产品,助力垂直行业端到端用户体验提升及客户生意增长