技术开发 频道

使用Janino优化Java的性能



   
【IT168 专稿】性能对于整个系统来说举足轻重。特别是在采用Java语言开发的大型多用户程序中更是如此。但遗憾的是,在性能问题并没发生之前,是比较难以辨别的,同时解决他们的成本也是很大的。在本文中,作者采用内嵌式的Janino编译库来列举优化Java代码的方法。当其他的办法在解决性能问题无效时,这些方法不免为可行之法。
       众所周知,越底层的代码,它的性能可能就越好。于是,通过编写较底层关键性能区代码常常可以提高程序的性能。而这种标准技术已经在很多的新型开发语言中广泛使用。从而,C语言变成了一种集装箱型语言,Java从C语言发展而来,而且JavaScript是从Java发展而来。
       对Java开发者来说,这种方法是痛苦的,因为Java的优势是能提供一个可以把应用程序与主机系统隔离的同质环境,而如果再转向底层的语言,Java的这种优势将不再拥有。但是,Java只是将应用程序逻辑进行直接编译。如此一来,它就可以有效的将域定义语言(domain-specific language)变成底层的Java字节码。
 
一、提高性能的传统方法
 
       Java程序的慢速构建已经影响了系统可见的性能。它会影响程序开发者的效率、开发成本及程序的可扩展性。那么,对于采用Java作为开发语言的公司,又将如何提到程序的性能呢?下面是几种常用的性能优化方法:
 
1. 优化算法
       在本文的实例中,将采用最安全且原始的方法来提到程序性能。仅通过改变本地代码而实现提高性能的目标,并且能简易地测试程序的可扩展性。
 
2. 优化硬件
       此种方法可以比较原始的提高程序的性能。相比开发者花费数小时去优化软件而言,通过改变单个服务器的机器性能更合算些。但是这种优化硬件的方法并没有考虑到如下一些较复杂的因素:开发人员的需要、应用程序被多次部署的可能性以及硬件能能被更新的空间。
 
3. 优化软件
       通常而言,Java代码(Java库、应用服务器、数据库驱动程序等)的运行效率较其它语言会更高些。尽量保证程序代码的高效性,是提高应用程序性能的不二选择。但遗憾的是,这往往很难预先决定,因为类库的性能特性取决于操作环境,而操作环境不到开发的最后阶段是不确定的。
 
4. 优化程序架构
       由于对人员素质要求较高,优化程序架构是最后的方法。通常而言,对程序的每一次架构上的变动,代码都需部分的重写并重新的评估。对于特大型的项目,局部的架构变动是很有可能的,而这种情况下优化程序架构的风险就相当的高。      
       如果为了提高程序的性能而穷尽以上的方法,那将是一件令人十分痛苦的事情,特别对一些复杂且难以理顺的大项目更是如此。那难道就没有其它更好的办法了?


二、Janino简介
 
       Janino是一种内嵌的开源Java编译器。Janino并不是一种提供给开发人员编译程序的编译器,而是Java程序在运行时编译Java代码的编译器。
       对使用过JSP的Java开发人员来说,Java源代码(通常内嵌于JSP中)动态编译成类文件是很熟悉的技术。但却未必知晓此技术的复杂性。而造成它比较笨拙的原因在于需要安装JDK(非JRE)。由于JDK并非免费,于是又出现了license的问题。此外,对于不同的平台,需进行不同的平台配置并部署相应的类文件。对简化版的程序也同样需要复杂的操作。
       尽管存在以上的问题,一些开源内库如Jasper仍基于JDK,且能提供很好的程序执行功能。基于Java的构建工具Ant同样面临在编译Java代码时需克服过于复杂的问题。基于以上所述,动态编译Java代码通常被认为是没有办法的办法,且在实际应用中表现欠佳。
 
       Janino采用如下的方式来解决这些问题:
1.    Janino如同普通的应用程序一样,运行在JVM上,而不是将编译的工作间接转交给javac(或是等同的Java编译器)来完成。同时也不需要额外的配置或安装JDK。
2.  由于Janino直接从JVM获得classes类文件,而不是通过应用程序获取class文件和.jar文件。这意味着无需考虑权限问题或是构建路径的配置。
3         Janino提供一种简单易用的方式来编译表达、脚本和classes类文件。开发人员无需关心加载动态生成代码的技术细节,因为Janino自动实现了它。在最简单的层面,传递一个包含Java代码的字符串即可返回一个对象。
Janino被更广范的应用于那些比较复杂、杂乱且与平台相关的Java应用程序的源码编译。Janino通过动态编译代码,从而提高了程序的性能。


三、使用Janino来优化Java代码
 
       在大程序中,都希望尽可能的提高程序的灵活性,但往往事与愿违。这意味着程序的很多功能都退化成了单个的可配置的功能点。例如,假设一个应用程序负责数据报表,且有众多的因素会制约报表的最后格式。包括用户现场环境、服务端环境、目前的数据集、用户喜好、用户权限等因素。这意味着程序的行为将被委派多次。而对大数据集更复杂的操作,这种转变无疑会降低程序的性能。而动态的代码编译则可以有效的解决此问题。
       这种方法有效的原因在于以下两个问题的答案:

1.       程序的配置运行时的程度
2.       性能问题在可配置代码中的局部化程度

如果以上两个问题都显得很重要,则动态编译对程序可能有益;否则,努力将有可能白费。自然而然,所需的成本也将相对的高些,于是程序也需相应的运行更有效率些。所以,专业类库所提供的“不透明”的服务(例如搜索、模式匹配或是数学运算等)将是不错的选择。
为了评价作者程序所采用的技术,作者写了一个叫BasicEvalFactory的工厂类。对于能完成基本的数学运算法则(加减乘除)语言,此类可以预解析它。程序的执行有意地比较基础化,其重点在于保证评估程序的高效性以及能产生一个优化的好性能结果。请记住,本例想比较Janino编译生成的代码与现成的已优化过的代码之间的性能差异。
BasicEvalFactory类的代码量有400行之多,而且包含大量的内部类;由于长度关系,这里不一一列举,整个评价程序的代码可以免费的下载(见资源部分)。所有的代码实现了如下的接口:

public interface Evaluator ...{ public double evaluate(Variables vars); }
计算参数表态式的评价方法类实现了如下的接口

public interface Variables { double getVariable(String name); }
接着作者使用Janino写了第二个实现。与第一个实现不同,第二个实现的代码量比较少,可以在这里列举出来。这是因为作者可以简单的把待评价的表达式转为Java表达式。此技术的优势在于Janino负责对表达式的解析工作,而由JVM负责完成评价工作。结果是,所需写的代码量变少了,因此可以在这里全部列出。它直接展现了如何使用Janino:

public class JaninoEvalFactory { private static Pattern PATTERN = Pattern.compile("([a-zA-Z]+)"); private static SimpleCompiler compiler = new SimpleCompiler(); public static Evaluator fromString(String string) { StringBuffer varCode = new StringBuffer(); Matcher matcher = PATTERN.matcher(string); Set names = new HashSet(); while (matcher.find()) { String name = matcher.group(0); if (names.contains(name)) continue; varCode.append("double " + name + " = vars.getVariable(\"" + name + "\");"); names.add(name); } String source = "package janinotest.eval;\n" +"public class JaninoEvaluator implements Evaluator {\n" +"\tpublic double evaluate(Variables vars) {\n" + "\t\t" +varCode + "\n" + "\t\treturn "+ string +";\n" + "\t}\n" +"}\n"; try { compiler.cook(new StringReader(source)); Class clss = compiler.getClassLoader().loadClass( "janinotest.eval.JaninoEvaluator"); Evaluator eval = (Evaluator) clss.newInstance(); return eval; } catch (Exception e) { throw new IllegalArgumentException(e.getMessage()); } } }
此类中的静态方法负责完成如下工作:
1.        表达式的参数定义为PATTERN类型,从包含Java代码的字符串利用上面定义过的Variables接口给其赋值。
2.        为了实现评价功能,这里联合使用了外部提供的表达式和一个基类的定义,以生成Java源。
3.        通过使用SimpleCompiler 对象将Java源编译成源代码。
4.        接着通过compiler类加载器来加载JaninoEvaluator类,并采用反射机制进行实例化。
5.        最后返回完成后的Evaluator对象。
例如,当提供的表达式为“(x + y)/(x - y) * 100/(x*y)”时,返回的源如下

public class JaninoEvaluator implements Evaluator { public double evaluate(Variables vars) { double x = vars.getVariable("x"); double y = vars.getVariable("y"); return (x + y)/(x - y) * 100/(x*y); } }
当然,使用Janino时也需要注意到它的局限性,尽管很少。其中关键的一点是Janino缺少对Java1.5版本新特色的支持,如对泛化及for-loop语法的支持。


四、性能比较
 
作者使用了三个不同复杂程度的表达来测试以上两种不同的实现方式,它们的结果如图1所示。启动程序的命令如下所示。为了能正常的运行性能测试程序,读者需要自己适当的调整classpath。
java -cp janino.jar;. janinotest.eval.JaninoTestEval
可想而知,表达式复杂性越大,Janino编译器表现出的优势越强。对中等复杂性的表达式而言,Janino编译时可以提高7倍的速度。当然,图1并不能提供一个性能提升基准,但能为开发人员提供一个可预见性的性能提高参考。
1.       0 (相同的)
2.       100 * x + 20 / 2 (简单的)
3.      (x + y)/(x - y) * 100/(x * y) (适度的)




对于正希望将Janino应用于自己项目的任何人而言,有一点很重要,就是将Janino用来优化已经建立的程序,其效果甚微。在前面作者建立的评价程序中,采用了$(令牌-名称)来代替令牌来测试Janino的性能加速情况。下图2所示,比较了如下的实现方式:
 
naive-map
一种使用Java的正则表达式取代令牌从地图生成字符串的简单但高效的实现方式。
fast-map
此方式是基于naïve-map并进行了优化,它可以将令牌化的字符串进行预先存储分解。
janino-map
可对代码进行预编译并能从地图值生成字符串。
fast-object
使用反射机制直接从POJO(Plain Old Java Object,普通Java类)抽取令牌值的优化实现方式。
janino-object
可对代码进行预编译并能从地图值生成字符串。
hardwired-object
此实现方式直接从特殊Java类抽取令牌值,以做为最可能的时间基准。



出人意料的是,采用JDK编译的代码(仅在此例中)其执行效率居然胜过了Janino的效率。同时,优化过的实现也快过编译版本的代码。这得归功于Sun的Java编译器而不是Janino。特别地,这两种不同的编译器所采用的实现策略是完全不一样的。但可喜的是,Janino的字符串处理功能已经优化了,同时它的性能可以与javac相媲美。
如果想亲自运行此性能测试程序,可以下载源程序(见资源部分),进行编译,并按如下的命令执行它(同样classpath要进行适当的修改):
 
java -cp janino.jar;. janinotest.sub.JaninoSubTest
 
因此,我们可得出结论,采用动态编译的优化技术是很有意义的,但只适合应用于性能可以明确提高的情况。因为,毕竟世上没有解决所有问题的方法。
 
五、结论
 
在平时的开发工作中,有很多情况可以用到动态编译的技术。如下则列出了一些基本的参考意见:
1.      使用动态编译类来取代Java代理类,可避免所有的反射,因此可显著的提高性能。
2.       对于用户自定义功能等请求,可以像Java代码一样进行编译与评价,而之前只能采用域定义语言(domain-specific language)来解析与评价。
3.        如果数据记录的字段固定,但不知道编译所需时间,此时是HashMaps应用的典型场景。利用Janino,已知的字段可用于构建私有字段。再加上一些额外的工作,可通过Map接口将字段暴露出来。如果字段类型相对记录数量来说很少时,内存的保存就显得很重要了。
 
在程序的性能优化方面Janino还有更多的应用场合,对于想更好的提高Java代码性能的开发人员,可以更加深入的研究Janino。
0
相关文章