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 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,这样比较会更加高效,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,514评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,142评论 25 707
  • 三爷发朋友圈说:“好看的皮囊千篇一律,有趣的灵魂万里挑一,有幸在简书遇到了有趣的灵魂”,哈哈,不知道我算不算他遇到...
    曹门霞客行阅读 545评论 3 17
  • 易效能天使6班3.0班43号沈家芬第四周检视 时间是一个人最稀缺的资源,时间管理的本质是精力管理。一个人的...
    7组61号沈家芬阅读 229评论 0 1
  • col
    CNSumi阅读 133评论 0 0