【问题标题】:Performance about the check() in Z3 for JavaZ3 for Java 中 check() 的性能
【发布时间】:2014-08-02 23:20:49
【问题描述】:

我正在使用 Z3 for Java 来检查具有未解释函数的术语的可满足性,例如 (type(o)>1 或 type(p)2 and type(o)=1)运行solver.check() 需要6 ms。

        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();

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

提前致谢。

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

【问题讨论】:

    标签: java performance z3


    【解决方案1】:

    6ms 对我来说听起来相当快。为什么这太慢了?

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

    如果应用程序对性能的要求如此之高,那么可能无法使用 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)。

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

    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 选项的行为会更糟。

    【讨论】:

    • 我不熟悉 Java 的本机互操作机制,但拨打电话肯定不需要 6 毫秒。用于检查的 .NET PInvoke 可能需要 100 条指令(?)。 Java 不会落后太多。
    • 单次通话可能是这样。对于 Z3 API 的每一次调用,都至少会再调用一次(用于检查错误),对于复杂的对象,还会有另一系列调用来找出 Java 应该将 AST* 转换为什么。 Java API(或 .NET、Python 等其他 API)并不是针对每次调用的原始性能而构建的。
    • @ChristophWintersteiger:谢谢。但是对于这个简单的约束,6ms 对我当前的系统来说确实是一个大问题,尤其是我们需要在一个典型的基准测试中调用求解器十亿次。由于整个系统是用Java实现的,我很难切换到c版本。
    • 我明白了,您能否更详细地描述一下这些约束是什么样的,以及为什么您希望它们能够如此迅速地得到解决?这里给出的例子是不能令人满意的。你认为这些问题中的大多数是无法解决的吗?该示例也以 .NET API 语法给出;您实际使用的是哪一个?在这些问题中,您是否总是有未解释的功能,如果有,有多少?它总是超过整数还是你有时会结合理论?
    • 我在问题中添加了更多分析信息,您能否检查该解决方案是否适用于这个特定的约束示例?
    【解决方案2】:

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

    【讨论】:

    • 没错,但“smt”本身就是一种非常复杂的策略,如果不进行预处理,可能会表现得更差。相反,如果问题可以转化为更简单的一类问题,则可能只使用预处理器来解决它们,但这并不一定总是可行的。
    • @usr:这很有帮助。我从solver.check() 切换到像MkTactic("smt") 这样的简单策略。与之前的 6ms 相比,它需要 1.5ms。我们能做得更好吗?
    • @YuFeng 我通常 grep Z3 源代码以找出战术是由什么组成的。尝试研究一些线性整数算术 (LIA) 逻辑。这些逻辑通常构建一个复杂的策略。这揭示了是什么使这项工作。查看 qflia_tactic.cpp。还尝试为“QF_LIA”逻辑制作求解器。我相信你的公式就是这样的逻辑。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-12
    • 1970-01-01
    • 2012-05-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多