比Velocity快10倍的模板引擎

在07年的时候,写过一个模板引擎,当时叫CommonTemplate,以前JavaEye有个开源系列介绍: http://www.iteye.com/news/3381,后来功能越来越多,性能却越来越差,在金大为发给我 性能对比结果后,看到惨不忍睹的差距,就打算抛弃原设计进行重写,但因工作忙,就搁置了,最近看温少发了几个EL和JSON的解析器,有点手痒,就抽了个周未,拿出来再改了改,主要将模板改成了字节码编译,并简化了语法及缩小使用范围,只针对HTML场景使用,并将名称改成了HTTL,名字含义是把HTML中的M(Marker)改成了T(Template),放在GoogleCode上: http://code.google.com/p/httl,性能和Java硬编码输出模板内容差不多,比Velocity/FreeMarker等快10倍左右。

语法方面的区别:
发现基于文本指令的,基于HTML标签的,基于HTML注释的,都有不少模板引擎实现,
为了标新立异以及使用的直观性,HTTL采用基于HTML属性的指令,如:
<table if="user.role == 'admin'">
    <tr foreach="book in books">
        <td>${book.title}</td>
    </tr>
</table>


选型方面的区别:
  • Velocity采用JavaCC编译成AST树,解释执行。
  • FreeMarker类似,只是采用FreeCC。
  • Smarty4j采用ASM生成字节码,对条件等字节码的生成比JDK稍逊,而且需在运行时判断context变量的类型,无法强类型编译模板。
  • HTTL采用先将模板转译成Java代码,再由JDK或Javassist编译成字节码,并在模板上声明传入类型,在编译期就推演所有变量类型,减少反射和运行时类型判断。

  • 部分优化策略示例:

    (1) 强类型编译
    对于表达式${user.name}的编译:
    Smarty4j弱类型字节码生成:
    Object user = context.get("user"); // 无法确定user是Map还是POJO
    // 反射获取属性的值,而且要运行期判断是user.getName(),还是user.name字段
    Object name = ReflectUtil.get(user, "name"); // 接下来name也要反射
    

    Httl强类型字节码生成:
    User user = (User)context.get("user"); // 通过in="User user"声明类型
    String name = user.getName(); // 编译期通过User的字段类型推演name的类型,并在编译期决定使用getName()
    

    如果只是编译成弱类型字节码,性能比解释执行快不了多少,淘宝编译Velocity的测试数据显示,只能比JavaCC的AST解释执行快10%左右,参见附件的PPT。(附件的PPT是蒋江涛在InfoQ大会的演讲稿,是公开的)

    (2) 对大模板拆分子函数:(未发布)
    SunJDK缺省对大于5K字节码的方法不进行JIT优化,
    所以当模板的内容较大时,会导致生成的字节码也比较大,
    通过拆分子函数,可以解决JIT优化问题。
    淘宝编译Velocity的测试数据显示,大模板拆分成小模板性能提升35%,参见附件的PPT。

    (3) 编译时就将文本编译成字节,加快输出:
    原编译:
    writer.write("<table><tr><td>");
    writer.write(user.getName());
    

    改为编译成:
    output.write( new byte[] {60, 116, 97, 98, 108, 101, 62, 60, 116, 114, 62, 60, 116, 100, 62};);
    output.write(user.getName().getBytes());
    

    淘宝编译Velocity的测试数据显示,将String输出预编译为byte[]输出,性能提升50%,参见附件的PPT。

    (4) 对同条件if语句优化:(未发布)
    if (user.role == "admin") {
        // ...
    } else if (user.role == "member") {
        // ...
    } else {
        // ...
    }
    

    优化后:
    int id = System.identityHashCode(user.role);
    switch (id) {
        case 3452345: // 编译时计算"admin"的identityHashCode
        // ...
        case 2342452:  // 编译时计算"member"的identityHashCode
        // ...
        default: 
        // ...
    }
    


    (5) 对于赋值生成的price为局部变量,不put回context:
    比如:set="price = price * discount / 100"编译:
    int price = price * discount / 100;
    

    除非声明out="price",才在模板渲染最后:
    context.put("price", price);
    


    (6) 减少int到Integer等元类型的boxed和unboxed,以及instanceof。
    因为模板输出的大量是基本类型和字符串,
    比如当输出基本类型时,需要转成String,如果使用format(Object)接口,就会将基本类型装箱,
    Httl遇到任何类似需要boxed和unboxed的地方,都会重载所有基本类型方法,以减少boxed和unboxed的处理。
    出现instanceof的地方也一样,会尽量多态处理。

    (7) 所有编译过程都会保持和计算源码位置,
    当出错时,错误信息能准确定位到出错行列。

    等等。

    性能测试:
  • 模板内循环显示100行数据。
  • 每模板各运行一万次。
  • 模板大小约800字符。
  • 模板每次运行输出内容约27K字符。
  • 测试类:PerformanceTest.java

  • 测试结果:
    Engine 编译时间 运行一万次时间 每秒处理数 模板 测试类
    Freemarker 125ms 16,934ms 590t/s books.ftl FreemarkerCase.java
    Velocity 110ms 19,278ms 518t/s books.vm VelocityCase.java
    Smarty4j 78ms 21,653ms 461t/s books.st Smarty4jCase.java
    Httl 547ms 2,077ms 4,814t/s books.httl HttlCase.java
    Java 0ms 2,016ms 4,960t/s Books.java JavaCase.java

    测试代码:
    http://code.google.com/p/httl/downloads/list

    更多信息参见:
    http://code.google.com/p/httl

    HTTL缺省使用Jdk的javax,tools编译字节码,需要500ms左右,如果换成Javassist编译,编译时间可以降到200ms左右,但字节码执行效率略差一点,但每个模板只编译一次,所以编译慢点也能忍受,如果想换成Javassist,只需在httl.properties中加入:
    compiler=com.googlecode.httl.support.compilers.JavassistCompiler
    java.version=1.4
    

    注:Javassist不支持1.5的语法,所以要设置java.version=1.4

    你可能感兴趣的:(Java综合)