國內外性能基準測試截圖
國外
國內一
國內二
如何輸出一個整型變量
常規來說,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發明創造,而是綜合了國內外其他模板工具,其他開源工具的各種性能優化方法。在此表示感謝