Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.Sign up
Unexpected huge performance difference between solver using "default" tactic, Z3_mk_solver() and Z3_mk_simple_solver() #1035
I'm currently investigating queries that Z3 is taking a very long time to solve in my tool and I ran into one that Z3 is handling in a surprising way.
This is the query
In my tool libz3 is taking ~1679 seconds to solve this. When I give this query to the Z3 binary it is solving the query in ~2 seconds.
I started digging into this and by making my tool increase libz3's verbosity it looks like the slow
but when Z3 binary solves the query a different tactic is being used.
In my tool I use the
To investigate some more I tried creating the Z3 solver instance in two different ways in my tool
theTactic = Z3_mk_tactic(builder->ctx, "default"); Z3_tactic_inc_ref(builder->ctx, theTactic); theSolver = Z3_mk_solver_from_tactic(builder->ctx, theTactic);
In both cases these changes to my tool caused performance to improve dramatically on these queries. Looking at the verbose output it seems when I instantiate Z3's solver this way it looks like the much faster
So why is this happening? I would have expected
Warning the interaction log has
Usually, Z3 will try to detect the logic (here) and then run a specialized tactic for the problem. This is what happens when the
However, when the
The solver obtained via
Usually, we would expect that specialized tactics are better at solving their respective problems, but that's of course not always true.
In this particular case, you should also see that Z3 switches to the SMT solver when one of the FP operations requires uninterpreted functions (e.g.,
A minor note: the "default" tactic tries to detect the logic after running the simplifier (see here). So, when it doesn't detect a matching logic, it will run the "smt" tactic on the simplified goal; so in these cases the actual tactic that gets executed is
@wintersteiger Thanks for the detailed response.
I didn't realise this is what the "simple" solver did. I thought it was just like
I think the doxygen documentation on
Currently the docs say
/** \brief Create a new (incremental) solver. This solver also uses a set of builtin tactics for handling the first check-sat command, and check-sat commands that take more than a given number of milliseconds to be solved. \remark User must use #Z3_solver_inc_ref and #Z3_solver_dec_ref to manage solver objects. Even if the context was created using #Z3_mk_context instead of #Z3_mk_context_rc. def_API('Z3_mk_solver', SOLVER, (_in(CONTEXT),)) */ Z3_solver Z3_API Z3_mk_solver(Z3_context c); /** \brief Create a new (incremental) solver. The function #Z3_solver_get_model retrieves a model if the assertions is satisfiable (i.e., the result is \c Z3_L_TRUE) and model construction is enabled. The function #Z3_solver_get_model can also be used even if the result is \c Z3_L_UNDEF, but the returned model is not guaranteed to satisfy quantified assertions. \remark User must use #Z3_solver_inc_ref and #Z3_solver_dec_ref to manage solver objects. Even if the context was created using #Z3_mk_context instead of #Z3_mk_context_rc. def_API('Z3_mk_simple_solver', SOLVER, (_in(CONTEXT),)) */ Z3_solver Z3_API Z3_mk_simple_solver(Z3_context c);
Fixing the descriptions here would hopefully prevent others from making my mistake.