Beetl模板語言:性能篇 頂 原 薦

國內外性能基準測試截圖

國外

輸入圖片說明

國內一

輸入圖片說明

國內二

輸入圖片說明

如何輸出一個整型變量

常規來說,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發明創造,而是綜合了國內外其他模板工具,其他開源工具的各種性能優化方法。在此表示感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章