如何输出一个整型变量
常规来说,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 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,这样比较会更加高效,
网友评论