Beetl模板语言:性能篇


声明:本文转载自https://my.oschina.net/xiandafu/blog/1584155,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

国内外性能基准测试截图

国外

输入图片说明

国内一

输入图片说明

国内二

输入图片说明

如何输出一个整型变量

常规来说,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发明创造,而是综合了国内外其他模板工具,其他开源工具的各种性能优化方法。在此表示感谢

本文发表于2017年12月04日 00:37
(c)注:本文转载自https://my.oschina.net/xiandafu/blog/1584155,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2693 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

万稳万当,不如一默。任何一句话,你不说出来便是那句话的主人,你说了出来,便是那句话的奴隶。

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1