国内外性能基准测试截图
国外

国内一

国内二

如何输出一个整型变量
常规来说,IO流提供了输出字符串(字符数组)的功能,所以,通常的整型输出应该是这样的代码:
String str = String.valueOf(12); out.write(str);
对于模板引擎来说,输出整形变量很常见,事实上,这个地方有非常大的性能提高空间。我们只要分析这俩句话的源码,就能看出,如何提高io输出int性能。 对于第一句 String.valueOf 实际上调用了Integer.toString(int i) 方法,此方法原代码如下
public static String toString(int i) { if (i == Integer.MIN_VALUE) return "-2147483648"; int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i); char[] buf = new char[size]; getChars(i, size, buf); return new String(buf, true); }
我们注意到,代码第5行分配了一个数组,对于任何一个高效的java工具来说,这都是个告警消息,分配数组耗时,垃圾回收也耗时
我们在分析out.write(str);代码,对于输出一个字符串,必须将字符串先转为字符串数组( 看到问题没有,这又回去了),熟悉String源码的同学都知道,这仍然是一个耗时操作,我们看一下源代码:
public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; }
如上代码,我们又发现了一次分配空间的操作,而且,还有一次字符串拷贝 System.arraycopy,这俩部又成了耗时操作
综合上面代码,我们就会发现,简单的一个int输出,除了基本的算法代码外,居然有俩次字符串的分配,还有一次数组copy。难怪性能低下(性能测试中确实这也是个消耗较多cpu的地方)。那么Beetl是如何改善的?
Beetl提供了一个专门的类IntIOWriter来处理字符串输出,如下关键代码片段:
public static void writeInteger(ByteWriter bw, Integer i) throws IOException { if (i == Integer.MIN_VALUE) { bw.writeString("-2147483648"); return; } int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i); char[] buf = bw.getLocalBuffer().getCharBuffer(); getChars(i, size, buf); bw.writeNumberChars(buf, size); }
如上代码,首先,我们可以看倒数第三行,并未分配字符素组,而是得到跟当前线程有关的一个char[] 其次,代码最后一行,直接就将此数组输出到IO流了,干净利索
综上所述,常规的输出int方法,除了常规算法外,需要俩次数组分配,和一次字符串拷贝操作。而Beetl则只需要常规算法即可输出,节省了俩次数组分配以及一次字符串copy操作。难怪性能这么好!
语言如何存取变量
于一个程序语言来说,访问变量是一个基本的操作,也是最频繁使用的操作。提高Beetl访问变量的效率,将整体上提高Beetl的性能,本文介绍了Beetl是如何访问变量的。 首先看一个简单的例子:
var a = "hi"; print(a);
第一行定义a变量,第二行引用a变量打印输出,通常设计下,可以在变量定义的时候将变量保存到map里,需要用的时候根据变量名取出。因此上诉代码可以翻译为java的类似如下代码: context.put("a","hi");
print(context.get("a");
尽管我们都知道Map存取都是非常快的,但还有没有更快的方式呢,答案就是有,那就是数组,数组的存取更快,通过如下代码可以看出, 数组的存放元素的速度是Map的10倍,读取那就更快了,是100倍
tring value1 = "a"; String value2 = "b"; String value3 = "c"; String key1 = "key1"; String key2 = "key2"; String key3 = "key3"; String[] objects = new String[3]; int loop = 10000 * 5000; //计算数组存消耗的时间 Log.key1Start(); for (int i = 0; i < loop; i++) { objects[0] = value1; objects[1] = value2; objects[2] = value3; } Log.key1End(); Map<String, String> map = new HashMap<String, String>(3); //计算Map存消耗的时间 Log.key2Start(); for (int i = 0; i < loop; i++) { map.put(key1, value1); map.put(key2, value2); map.put(key3, value3); } Log.key2End(); // 计算数组取消耗的时间 Log.key3Start(); for (int i = 0; i < loop; i++) { value1 = objects[0]; value2 = objects[1]; value3 = objects[2]; } Log.key3End(); // 计算map取消耗的时间 Log.key4Start(); for (int i = 0; i < loop; i++) { value1 = map.get(key1); value2 = map.get(key2); value3 = map.get(key3); } Log.key4End(); //打印性能统计数据 Log.display("使用数组设置", "使用Map设置", "使用数组读取", "使用map读取");
控制台输出:
======================
使用数组设置=139 百分比,Infinity 使用Map设置=1020 百分比,Infinity 使用数组读取=3 百分比,Infinity 使用map读取=767 百分比,Infinity*
Beetl在修改2.0引擎的时候,对变量存取进行了优化,使用一个一维数组来保存变量,如本文开头的例子
,在2.0引擎里,翻译成如下代码:
context.vars[varNode.index] = "hi" print(context.vars[varNode.index]);
那么,Beetl又是怎么做给模板变量分配索引呢?如下代码是如何分配索引的?
var a = 0; { var b = 2; } { var c = 2; } var d =1 ;
虽然有4个变量,但维护这些变量的只需要一个一维数组就可以,数组长度是3
节点a,d,c,b的index是0,1,2,2,就是子context(进入block后) 会在上一级context后面排着:先分配顶级变量a和d,赋上索引是0和1,然后二级变量b赋值索引是2,对于同样是二级的变量c,也可以赋上索引为2,因为变量b的已经出了作用域。
经过性能测试证明2.0的性能关于变量赋值和引用,综合提高了50倍,这也就是模板越复杂,Beetl性能越高的原因
日期格式化的小改动,性能大变化
模板语言里,经常内置了日期格式化函数,如Beetl提供了日期格式化:
${date(),"yyyy-MM-dd"}
别小看日期格式化,用好了会带来极高的性能,这是因为日期格式化使用了java自带的SimpleDateFormat,这是一个重量级对象,如果每次格式化都创建这样一个对象,非常不划算,因此可以缓存此对象,考虑到SimpleDateFormat是线程不安全的,因此使用ThreadLocal来缓存,Beetl的实现如下
/** * 日期格式化函数,如 * ${date,dateFormat='yyyy-Mm-dd'},如果没有patten,则使用local * [@author](https://my.oschina.net/arthor) joelli * */ public class DateFormat implements Format { private static final String DEFAULT_KEY = "default"; private ThreadLocal<Map<String, SimpleDateFormat>> threadlocal = new ThreadLocal<Map<String, SimpleDateFormat>>(); public Object format(Object data, String pattern) { if (data == null) return null; if (Date.class.isAssignableFrom(data.getClass())) { SimpleDateFormat sdf = null; if (pattern == null) { sdf = getDateFormat(DEFAULT_KEY); } else { sdf = getDateFormat(pattern); } return sdf.format((Date) data); } else if (data.getClass() == Long.class) { Date date = new Date((Long) data); SimpleDateFormat sdf = null; if (pattern == null) { sdf = getDateFormat(DEFAULT_KEY); } else { sdf = getDateFormat(pattern); } return sdf.format(date); } else { throw new RuntimeException("参数错误,输入为日期或者Long:" + data.getClass()); } } private SimpleDateFormat getDateFormat(String pattern) { Map<String, SimpleDateFormat> map = null; if ((map = threadlocal.get()) == null) { /** * 初始化2个空间 */ map = new HashMap<String, SimpleDateFormat>(4, 0.65f); threadlocal.set(map); } SimpleDateFormat format = map.get(pattern); if (format == null) { if (DEFAULT_KEY.equals(pattern)) { format = new SimpleDateFormat(); } else { format = new SimpleDateFormat(pattern); } map.put(pattern, format); } return format; } }
getDateFormat 方法就是从ThreadLocal里取出一个缓存,缓存的Key值就是pattern
IO 优化
Beetl主要用于模板输出,对于绝大部分模板来说,静态文本是主要的。Beetl模板不仅仅缓存了这些静态文本,而且,提前将这些静态文本转化为字节流。因此,渲染模板输出的时候,节省了大量转码时间,对于如下java代码输出
writer.println("你好");
在实际使用的时候,java会将你好转为字节码再输出,类似如下
byte[] bs = "你好".getBytes(); out.write(bs);
为了避免在大量输出静态文本过程中的转码(这是一个相当耗时间的操作),Beetl会事先存储静态文本的二进制码并作为一个变量放到Context.staticTextArray数组里(记得上一节讲过,数组的存取速度是逆天的快)。并提供一个ByteWriter类来支持同时操作char和byte
不起眼的for循环优化
对于任何语言来说,都必须支持循环,也必须支持循环跳转,如break;continue; 对于模板语言的实现过程中,for循环都需要检测是否有跳转命令,这无疑耗费了性能,如下是常规实现
while (it.hasNext()) { ctx.vars[varIndex] = it.next(); forPart.execute(ctx); switch (ctx.gotoFlag) { case IGoto.NORMAL: break; case IGoto.CONTINUE: ctx.gotoFlag = IGoto.NORMAL; continue; case IGoto.RETURN: return; case IGoto.BREAK: ctx.gotoFlag = IGoto.NORMAL; return; } }
也就是forPart.execute(ctx);每次执行完,都需要判断是否有跳转发生。 尽管从语言来看,switch效率足够的高,但是否还能优化呢,因为有的模板渲染逻辑里for语句没有使用跳转? 答案是能,Beetl在语法解析阶段就能分析到for语句里是否包含有break,continue等指令,从而判断这个for语句是否要判断跳转,因此,在ForStatement实现里,实际代码是
if (this.hasGoto) { while (it.hasNext()) { ctx.vars[varIndex] = it.next(); forPart.execute(ctx); switch (ctx.gotoFlag) { case IGoto.NORMAL: break; case IGoto.CONTINUE: ctx.gotoFlag = IGoto.NORMAL; continue; case IGoto.RETURN: return; case IGoto.BREAK: ctx.gotoFlag = IGoto.NORMAL; return; } } } else { while (it.hasNext()) { ctx.vars[varIndex] = it.next(); forPart.execute(ctx); } } }
hasGoto 代表了语法解析结果,这是在Beetl分析模板的时候得出的结果。
再强调一次的char[] 优化。
模板引擎涉及大量的字符操作,难免会有如下代码
char[] cs = new char[size];
这种需要分配内存空间的操作又是一个非常耗时间的操作,这种代码会出现在beetl引擎很多地方,也会出现在JDK里的一些工具类里,比如在第一节“如何输出一个整型变量“,可以看到,将JDK内置的
char[] buf = new char[size];
变成
char[] buf = bw.getLocalBuffer().getCharBuffer();
getCharBuffer 返回了一个已经分配好的char数组,这在一个模板渲染过程中实现有效并可重用,具体代码可以参考 ContextLocalBuffer.java
public class ContextLocalBuffer { /** * 初始化的字符数组大小 */ public static int charBufferSize = 256; /** * 初始化的字节大小 */ public static int byteBufferSize = 256; private char[] charBuffer = new char[charBufferSize]; private byte[] byteBuffer = new byte[byteBufferSize]; static ThreadLocal<SoftReference<ContextLocalBuffer>> threadLocal = new ThreadLocal<SoftReference<ContextLocalBuffer>>() { protected SoftReference<ContextLocalBuffer> initialValue() { return new SoftReference(new ContextLocalBuffer()); } }; public static ContextLocalBuffer get() { SoftReference<ContextLocalBuffer> re = threadLocal.get(); ContextLocalBuffer ctxBuffer = re.get(); if (ctxBuffer == null) { ctxBuffer = new ContextLocalBuffer(); threadLocal.set(new SoftReference(ctxBuffer)); } return ctxBuffer; } public char[] getCharBuffer() { return this.charBuffer; } // 忽略其他代码 }
反射调用性能增强
对于模板中任何输出对象,都需要通过java反射掉用对象属性,比如
${user.name}
实际上是在Beetl引擎种是大概如下调用
Class c = obj.getClass(); Method m = c.getMethod("getName",new Class[0]); Object ret = m.invoke(c,new Object[0]);
反射操作是个相当耗时间的操作,即使到了JDK8做了大量性能提升,也远远不如直接调用user.getName() 快。因此Beetl模板引擎在启用FastRuntimeEngine的情况下,可以优化这一部分调用,将反射调用转为为直接调用,以user.name 调用为例子,FastRuntimeEngine会编译这个代码为直接调用
Objec ret = User$name.call(obj);
User_name是动态生成字节码,其源码
public class User$name{ public Object call(Object o){ return ((User)o).getName(); } }
动态生成字节码的代码在FieldAccessBCW.java, 部分代码如下
public void write(DataOutputStream out) throws Exception { //第一个占位用 out.writeInt(MAGIC); out.writeShort(0); //jdk5 out.writeShort(49); int clsIndex = this.registerClass(this.cls); int parentIndex = this.registerClass(this.parentCls); byte[] initMethod = getInitMethod(); byte[] valueMethod = this.getProxyMethod(); //constpool-size out.writeShort(this.constPool.size() + 1); writeConstPool(out); out.writeShort(33);//public class out.writeShort(clsIndex); out.writeShort(parentIndex); out.writeShort(0); //interface count; out.writeShort(0); //filed count; //写方法 out.writeShort(2); //method count; out.write(initMethod); out.write(valueMethod); out.writeShort(0); //class-attribute-info }
如果你不熟悉字节码,可以参考我的一个博客 http://blog.csdn.net/xiandafu/article/details/51458791 另外一款模板引擎webit有高效的实现,他生成的虚拟代码类似如下
public Class UserAccessor(){ public Object get(Object o,String attName){ int hasCode = attName.hasCode(); switch(hashCode){ case 1232323:return ((User)o).getName(); case 45454545:return ((User)o).getAge(); } } }
假设“name”的hascode是1232323,"age"的hascode是45454545,这样比较会更加高效。
最后说明
Beetl性能优化还用了其他很多方法,在这里就不再讲述了。采用的这些优化方法,并非是Beetl发明创造,而是综合了国内外其他模板工具,其他开源工具的各种性能优化方法。在此表示感谢