关于Z3 for Java中check()的性能

时间:2014-08-02 23:20:49

标签: java performance z3

我正在使用Z3 for Java来检查具有未解释函数的术语的可满足性,例如(type(o)> 1或type(p)< 1)。我遇到了check()函数导致的性能问题。例如,运行solver.check()需要6 ms来处理一个非常简单的约束(类型(o)> 2和type(o)= 1)。

        FuncDecl typeFun = ctx.MkFuncDecl("type", ctx.IntSort(), ctx.IntSort());        
        Expr o = ctx.MkConst("o", ctx.IntSort());
        //type(o)
        IntExpr to = (IntExpr)typeFun.Apply(o);
        //type(o)=1
        BoolExpr subExpr1 = ctx.MkEq(to, ctx.MkInt("1"));   
        //type(o)>2    
        BoolExpr subExpr2 = ctx.MkGt(to, ctx.MkInt("2"));  
        //type(o)>2 and type(o)=1
        BoolExpr expr = ctx.MkAnd(new BoolExpr[] { subExpr1, subExpr2 });

        solver.Assert(expr);
        //this step will take 6 ms.
        solver.check();

鉴于我的项目中实际约束的大小要大得多(但是每个术语都非常简单,例如类型(o1)= 1,类型(o2)> 1等)比这个例子还有数十亿这种约束需要解决:
1. check()的性能应该是这样的吗? 2.如果1的答案为是,是否有其他替代方法可以绕过性能问题?

提前致谢。

@ChristophWintersteiger: 我认为我系统中的很大一部分限制应该是SAT。我正在实现Java的指针分析,我正在使用Z3以自下而上的方式解决虚拟调用的潜在目标。假设我有一个虚拟的callsite v.foo(),这个callsite可能会根据v的动态类型调用不同的方法。所以对于每个被调用者foo(),我将引入一个约束类型(o)= T其中o是点 - 接收器v和T的集合是声明foo的类。约束意味着v.foo()可以调用T中的方法foo(),当它的一个动态点集合的类型为T.我当前系统中的所有约束都是一些线性算术,只有一个未解释的函数“类型(○)”。但由于我是以自下而上的方式分析调用图,因此可以扩展与每个虚拟调用点相关的约束,利用分析到达根级别并且已经解决了接收器的所有点到目标。

2 个答案:

答案 0 :(得分:2)

对我来说,6毫秒听起来很快。为什么这太慢了?

solver.check()调用本机.dll / .so,因此在执行调用之前会涉及相当多的开销。一旦.check()返回,检查结果是否有错误就会产生另一些开销。因此,它可能需要6毫秒,因为应用程序正在使用Java API。

如果应用程序对如此高的性能至关重要,则可能无法使用C API,因为所有其他API都会产生一些非零开销。

附录:我已经对以下简单类型进行了一些分析:

long startTime = System.nanoTime();
int i = 0;
for (; i < 10000; i++) {                                
    status = solver.check();
}
long elapsedTime = System.nanoTime() - startTime;

运行大约需要22秒。在此处调用的默认解算器确实在此特定实例上表现出次优性能。我们可以通过添加ignore_solver1选项(或使用push / pop命令)强制Z3回退到较旧的解算器,即按如下方式设置它:

Solver solver = ctx.mkSolver();
Params solver_params = ctx.mkParams();
solver_params.add("ignore_solver1", true);
solver.setParameters(solver_params);

解决相同问题10k次现在需要~100ms(我得到9us / check)。

在这些命令上调用的解算器也支持增量求解,因此我们几乎可以免费添加push / pop命令:

for (; i < 10000; i++) {
    solver.push();
    status = solver.check();
    solver.pop();
}

将10k检查的运行时间增加到~120ms。现在,将solver.add(expr)移动到此循环会大大增加运行时间,即为此:

for (; i < 10000; i++) {
    solver.push();
    solver.add(expr);
    status = solver.check();
    solver.pop();
}

我的运行时间约为275毫秒,即接近我们之前的两倍。因此,此时,表达构建/添加所需的时间与求解时间具有相同的数量级。此时,瓶颈确实是Java API。

我或许应该补充一点,我只尝试了问题中提供的示例。很可能情况是,在稍微不同的示例中,求解器的行为不同,添加ignore_solver1选项会表现得更糟。

答案 1 :(得分:1)

您可以尝试使用自定义策略。默认求解器使用了相当广泛的算法组合(我相信)。尝试使用简单的策略而不进行预处理,例如MkTactic("smt")