复杂的动态布尔表达式性能评估(2)--Groovy实现
前言:
规则引擎中, 往往涉及到多个条件构成了复杂布尔表达式的计算. 对于这类布尔表达式, 一是动态可变的(取决于运营人员的设定), 二是其表达式往往很复杂. 如何快速的计算其表达式的值, 该系列文章将以两种方式, Antlr4动态生成AST(抽象语法树), 以及Groovy动态编译的方式来对比评估, 看看哪种方式性能更优, 以及各自的优缺点. 本篇文章将侧重于Groovy的实现思路.
模型简化:
每个规则可以理解为多个条件构建的复杂布尔表达式, 而条件本身涉及不同的变量和阈值(常量), 以及中间的操作符(>=, >, <, <=, !=, =).
比如某个具体的规则:
rule = expr1 && (expr2 || expr3) || expr4
而其具体条件expr1/expr2/expr3/expr4如下:
expr1 => var1 >= 20 expr2 => var2 != 10 expr3 => var3 < 3.0 expr4 => var4 = true
为了简化评估, 我们简单设定每个条件就是一个布尔变量(bool). 这样每个规则rule就可以理解为多个布尔变量, 通过&&和||组合的表达式了, 简单描述为:
rule = 1 && (2 || 3) || 4
数字N(1,2,...)为具体的布尔变量, 类似这样的简化模型, 方便性能评估.
Groovy实现:
先配置maven的依赖.
1 2 3 4 5 | < dependency > < groupId >org.codehaus.groovy</ groupId > < artifactId >groovy-all</ artifactId > < version >2.4.13</ version > </ dependency > |
然后编写Groovy脚本的执行工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | package com.dsl.perfs; import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.runtime.InvokerHelper; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class GroovyShellUtils { private static ConcurrentHashMap<String, Class> scriptClassMap = new ConcurrentHashMap(); public static <T> T execExpr(String expr, Map<String, Object> params, Class<T> returnType) { if (expr == null || expr.length() == 0 ) { return null ; } else { Object result = null ; try { Class e = parseClass(expr); result = InvokerHelper.createScript(e, new Binding(params)).run(); return (T)result; } catch (Exception var5) { return null ; } } } public static Class parseClass(String scriptText) throws CompilationFailedException { String key = keyGen(scriptText); Class value = (Class)scriptClassMap.get(key); if (value != null ) { return value; } else { synchronized (scriptText.intern()) { if (scriptClassMap.get(key) == null ) { GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader()); Class scriptClass = groovyClassLoader.parseClass(scriptText); scriptClassMap.put(key, scriptClass); return scriptClass; } } return (Class)scriptClassMap.get(key); } } private static String keyGen(String script) { return String.valueOf(script.intern().hashCode()); } } |
具体执行时, 采用一个trick的方式, 将数值变量化(统一添加变量名前缀).
比如把表达式:
1 && 2 || 3 || 4 && (5 || 6)
转化为
t1 && t2 || t3 || t4 && (t5 || t6)
测试评估:
具体的测试代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | package com.dsl.comp; import com.dsl.perfs.GroovyShellUtils; import java.util.Map; import java.util.Random; import java.util.TreeMap; public class AntlrPerf { public static void main(String[] args) { String boolExpr = "1 && 2 || 3 || 4 && (5 || 6)" ; int iterNums = 1000000 ; long randomSeed = 10001L; String nboolExpr = boolExpr; nboolExpr = nboolExpr.replace( "1" , "t1" ); nboolExpr = nboolExpr.replace( "2" , "t2" ); nboolExpr = nboolExpr.replace( "3" , "t3" ); nboolExpr = nboolExpr.replace( "4" , "t4" ); nboolExpr = nboolExpr.replace( "5" , "t5" ); nboolExpr = nboolExpr.replace( "6" , "t6" ); long beg = System.currentTimeMillis(); random.setSeed(randomSeed); for ( int i = 0 ; i <= iterNums; i++) { Map<String, Object> params = new TreeMap<>(); params.put( "t1" , random.nextBoolean()); params.put( "t2" , random.nextBoolean()); params.put( "t3" , random.nextBoolean()); params.put( "t4" , random.nextBoolean()); params.put( "t5" , random.nextBoolean()); params.put( "t6" , random.nextBoolean()); GroovyShellUtils.execExpr(nboolExpr, params, Boolean. class ); } long end = System.currentTimeMillis(); System.out.println(String.format( "total consume: %dms" , end - beg)); } } |
测试结果如下:
total consume: 1039ms
和上篇Antlr4方案的测试结果755ms, 1039ms相对慢一些, 但总结而言差不多, 事实上, 无论采用哪种方案, 对于具体的线上服务而言, 其永远不是主要的性能瓶颈.
优缺点分析:
从性能结果上看, Antlr4动态解析的方案有一定的优势. 另一方面, 采用Groovy的方案, 对应的表示式会生成一个对应的Class类, 表达式越多, 生成的Class越多, 对方法区的消耗也不小. 由于JIT的存在, 会将热点的代码编译生成native code, 用于代码的加速执行. 但是该native code区域的空间相对较小, 满了会影响性能.
但是从灵活性和场景适用范围而言, Groovy方案几乎完胜, Antlr4的编码成本太高, 尤其是面对复杂的逻辑时.
总结:
本文也是借助复杂布尔表达式的评估, 来简单比较下Antlr方案和Groovy方案的差异. 条条大路通罗马, 其实那个方案都合理.
posted on 2018-02-28 11:35 mumuxinfei 阅读(1417) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构