This repository has been archived on 2025-03-06. You can view files and clone it, but cannot push or open issues or pull requests.
dekker-phd-thesis/chapters/4_rewriting.tex
2021-03-09 15:55:10 +11:00

1109 lines
53 KiB
TeX

%************************************************
\chapter{Rewriting Constraint Modelling Languages}\label{ch:rewriting}
%************************************************
High-level \cmls, like \minizinc, were originally designed as a convenient input
language for \gls{cp} solvers \autocite{marriott-1998-clp}. A user would write a
model consisting of a few loops or comprehensions; given a data file for the
parameters, this would be rewritten into a relatively small set of constraints
which would be fed whole into the solver. The existing process for translating
\minizinc\ into solver-specific constraints is a somewhat ad-hoc, (mostly)
single-pass, recursive unrolling procedure, and many aspects (such as call
overloading) are resolved dynamically. In its original application, this was
acceptable: models (both before and after translation) were small, translation
was fast, and the expected results of translation were obvious. The current
architecture is illustrated in \Cref{sfig:4-oldarch}.
But over time, the uses of high-level \cmls\ have expanded greatly from this
original vision. It is now used to generate input for wildly different solver
technologies: not just constraint programming, but also MIP
\autocite{wolsey-1988-mip}, SAT \autocite{davis-1962-dpll} and Constraint-based
Local Search \autocite{bjordal-2015-fzn2oscarcbls} solvers. Crucially, the same
constraint model can be used with any of these solvers, which is achieved
through the use of solver-specific libraries of constraint definitions. In
\minizinc, these solver libraries are written in the same language.
\minizinc\ turned out to be much more expressive than expected, so more
and more preprocessing and model transformation has been added to both the core
\minizinc\ library, and users' models. And in addition to one-shot solving,
\minizinc\ is frequently used as the basis of a larger meta-optimisation tool
chain: programmatically generating a model, solving it, then using the results
to generate another slightly different model. This usage is represented by the
dashed feedback loop in \Cref{sfig:4-oldarch}.
\begin{figure}[ht]
\centering
\begin{subfigure}[b]{.45\columnwidth}
\centering
\includegraphics[width=\columnwidth]{assets/img/4_old_process}
\caption{\label{sfig:4-oldarch}}
\end{subfigure}%
\hspace{0.05\columnwidth}%
\begin{subfigure}[b]{.50\columnwidth}
\centering
\includegraphics[width=\columnwidth]{assets/img/4_new_process}
\caption{\label{sfig:4-newarch}}
\end{subfigure}
\caption{\label{fig:4-arch} (a) Current \minizinc\ architecture, and (b) new
\minizinc\ architecture}
\end{figure}
To a great extent, this is testament to the effectiveness of the language.
However, as they have become more common, these extended uses have revealed
weaknesses of the existing \minizinc\ tool chain. In particular:
\begin{itemize}
\item Flattening is inefficient. The flattener does a surprisingly large
amount of work for each expression (especially resolving sub-typing and
overloading), which may be repeated many times --- for example, inside
the body of a comprehension. And as models generated for other solver
technologies (particularly MIP) can be quite large, the resulting
flattening procedure can be intolerably slow. As the model
transformations implemented in \minizinc\ become more sophisticated,
these performance problems are simply magnified.
\item The generated models often contain unnecessary constraints. During the
transformation, functional expressions are replaced with constraints.
But this breaks the functional dependencies: if the original expression
later becomes redundant (due to model simplifications), \minizinc\ may
fail to detect that the constraint can be removed.
\item Monolithic flattening is wasteful. When \minizinc\ is used for
multi-shot solving, there is typically a large base model common to all
subproblems, and a small set of constraints which are added or removed
in each iteration. But with the existing \minizinc\ architecture, the
whole model must be re-flattened each time.
% Many use cases involve generating a base model, then repeatedly adding
% or removing a few constraints before re-solving. In the current tool
% chain, the whole model must be fully re-flattened each time. Not only
% does this repeat all the work done to flatten the base model,
This means a large (sometimes dominant) portion of runtime is simply
flattening the core model over and over again. But it also prevents
\emph{the solver} from carrying over anything it learnt from one problem
to the next, closely related, problem.
\end{itemize}
In this chapter, we revisit the rewriting of high-level \cmls, like \minizinc,
into solver-level constraint models. We describe a new \textbf{systematic view
of the execution of \cmls}, and build on this to propose a new tool chain. We
show how this tool chain allows us to:
\begin{itemize}
\item easily integrate a range of \textbf{optimisation and simplification}
techniques,
\item effectively \textbf{detect and eliminate dead code} introduced by
functional definitions, and
\item support \textbf{incremental flattening and solving}, and better
integration with solvers providing native incremental features.
\end{itemize}
The new architecture is shown in \Cref{sfig:4-newarch}. The model is first
compiled to byte code (called \microzinc), independent of the data. The byte
code is interpreted with the data to produce \nanozinc\ code, an extension of
the existing \flatzinc\ format. The interpreter can even be made incremental: in
\cref{ch:incremental} we discuss how in meta optimisation, no recompilation is
required.
We have developed a prototype of this tool chain, and present experimental
validation of these advantages. The prototype is still very experimental, but
preliminary results suggest the new tool chain can perform flattening much
faster, and produce better models, than the current \minizinc\ compiler.
The rest of the paper is organised as follows. \Cref{sec:4-micronano} introduces
the \microzinc\ and \nanozinc\ languages, the new intermediate representation we
propose that enables more efficient flattening. \Cref{sec:4-simplification}
describes how we can perform various processing and simplification steps on this
representation, and in \cref{sec:4-experiments} we report on the experimental
results of the prototype implementation. Finally, \Cref{sec:4-conclusion}
presents our conclusions.
\section{\glsentrytext{microzinc} and
\glsentrytext{nanozinc}}\label{sec:4-micronano}
This section introduces the two sub-languages of \minizinc\ at the core of the
new abstract machine model we have developed.
MicroZinc (\microzinc) is a simplified subset of \minizinc\ that only contains
function declarations and a restricted expression language. Models written in
different \cmls\ can be translated into \microzinc. NanoZinc (\nanozinc) is
similar to \flatzinc\ in that it contains only simple variable declarations and
flat constraints. However, while all function calls in \flatzinc\ need to be
solver-level primitives, \nanozinc\ calls can refer to functions implemented in
\microzinc.
The core of the syntax of \microzinc\ is defined in \cref{fig:4-uzn-syntax}. We
have omitted the definitions of \syntax{<type-inst>} and \syntax{<ident>}, which
you can assume to be the same as in the definition of \minizinc. While we omit
the typing rules here for space reasons, we will assume that in well-typed
\microzinc\ programs, the conditions \syntax{<exp>} in if-then-else and where
expressions have \pari\ type.
\begin{figure}
\begin{grammar}
<model> ::= <fun-decl>*
<fun-decl> ::= function <type-inst>: <ident>([<type-inst>: <ident>]*) [ = <exp> ];
<literal> ::= <constant> | <ident>
<exp> ::= <literal>
\alt <ident>( <literal>* )
\alt let { <item>* } in <exp>
\alt if <exp> then <exp> else <exp> endif
\alt [ <exp> | <gen-exp> where <exp> ]
<item> ::= <type-inst>: <ident> [ = <exp> ];
\alt constraint <exp>;
<gen-exp> ::= <ident> in <exp> [ , <ident> in <exp> ]*
\end{grammar}
\caption{\label{fig:4-uzn-syntax} Syntax of \microzinc.}
\end{figure}
\newcommand{\sep}{\rhd}
A \nanozinc\ program, defined in \cref{fig:4-nzn-syntax}, is simply a list of
calls, where the result of each call is bound to either an identifier or the
constant true. The \syntax{$\sep$ <ident>*} will be used to track dependent
constraints (this will be explained in detail in \cref{sec:4-nanozinc}, for now
you can assume it is empty).
\begin{figure}
\begin{grammar}
<nzn> ::= <nzn-def>*
<nzn-def> ::= <nzn-result> $\mapsto$ <nzn-bind> $\sep$ <ident>*
<nzn-bind> ::= <ident> | <nzn-call>
<nzn-result> ::= <ident> | true
<nzn-call> ::= <ident>( <literal>* )
\end{grammar}
\caption{\label{fig:4-nzn-syntax}
Syntax of \nanozinc.
}
\end{figure}
\subsection{\glsentrytext{minizinc} to
\glsentrytext{microzinc}/\glsentrytext{nanozinc}}
The translation from \minizinc\ to \microzinc\ involves the following steps:
\begin{enumerate}
\item Transform all expressions that are valid in \minizinc\ but not in
\microzinc. This includes simple cases such as replacing operator
expressions like \mzninline{x+y} by function calls
\mzninline{int_plus(x,y)}, and more complex rules for rewriting
conditionals with variables (as mentioned above), optional variables
into non-optional forms \autocite{mears-2014-option}, lifting partiality
in expressions into the surrounding context
\autocite{stuckey-2013-functions}, and statically resolving
subtype-based overloading.
\item Transform all function definitions so that they do not refer to
top-level variables, by adding those variables as extra arguments to the
function declaration and each call site
\item Move all top-level variable declarations and constraints into a let
expression in the body of a new function called \mzninline{main}, whose
arguments are the parameters of the model.
\end{enumerate}
% \begin{algorithm}[H]
% \KwData{A \minizinc\ model $M = \langle{}V, C, F\rangle{}$, where $V$ is a set of declaration
% items, $C$ is a set of constraint items, and $F$ are the definitions for all
% functions used in $M$.}
% \KwResult{A list of \microzinc\ defintions $\mu$ and a \nanozinc\ program $n$.}
% \While{not at end of this document}{
% read current\;
% \eIf{understand}{
% go to next section\;
% current section becomes this one\;
% }{
% go back to the beginning of current section\;
% }
% }
% \caption{MicrowaveModel}
% \end{algorithm}
% \begin{algorithm}[H]
% \KwData{A \minizinc\ expression $e$ and $rn$ a function to rename identifiers}
% \KwResult{A \microzinc\ expression $e'$.}
% \Switch{$e$} {
% \Case{\textup{\texttt{if \(c\) then \(e_{1}\) else \(e_{2}\) endif}}}{
% \eIf{\(type(c) \in \textup{\texttt{var}}\)}{
% \(e' \longleftarrow \texttt{MicroExpr}(\texttt{slv\_if\_then\_else(\(c\), \(e_{1}\), \(e_{2}\))}, rn)\)
% }{
% \(c' \longleftarrow \texttt{MicroExpr(\(c, rn\))}\)\;
% \(e_{1}' \longleftarrow \texttt{MicroExpr(\(e_{1}, rn\))}\)\;
% \(e_{2}' \longleftarrow \texttt{MicroExpr(\(e_{2}\), rn)}\)\;
% \(e' \longleftarrow \texttt{if \(c'\) then \(e_{1}'\) else \(e_{2}'\) endif}\)
% }
% }
% \Case{\textup{(call)} \(n(e_{1}, ..., e_{n})\)}{
% }
% \lCase{\textup{(ident)} $id$} {\(e' \longleftarrow rn(id)\)}
% \lOther{\(e' \longleftarrow e\)}
% }
% \Return{\(e'\)}
% \caption{MicroExpr}
% \end{algorithm}
\begin{example}
Consider the following \minizinc\ model, a simple unsatisfiable ``pigeonhole
problem'', trying to assign $n$ pigeons to $n-1$ holes:
\begin{mzn}
predicate all_different(array[int] of var int: x) =
forall (i,j in 1..length(x) where i<j) (
x[i] != x[j]
);
int: n;
array[1..n] of var 1..n-1: pigeon;
constraint all_different(pigeon);
\end{mzn}
The translation to \microzinc\ turns all expressions into function calls,
replaces the special generator syntax in the \mzninline{forall} with an array
comprehension, and creates the \mzninline{main} function. Note how the domain
$1..n-1$ of the \mzninline{pigeon} array is replaced by a \mzninline{set_in}
constraint for each element of the array.
\begin{mzn}
predicate all_different(array[int] of var int: x) =
forall([
int_neq(element(x,i),element(x,j))
| i in range(1,length(x)),
j in range(1,length(x))
where int_lt(i,j)
]);
predicate main(int: n) = let {
array[range(1,n)] of var int: pigeon;
constraint forall([
set_in(element(pigeon,i), range(1,int_minus(n,1)))
| i in range(1,n)
])
constraint all_different(pigeon);
} in true;
\end{mzn}
For a concrete instance of the model, we can then create a simple \nanozinc\
program that calls \mzninline{main}, for example for $n=10$:
\mzninline{true @$\mapsto$@ main(10) @$\sep$@ []}.
\end{example}
\subsection{Partial Evaluation of \glsentrytext{nanozinc}}\label{sub:4-partial}
Each call in a \nanozinc\ program is either a primitive, or it has a
corresponding \microzinc\ definition. The goal of partial evaluation is to
transform the \nanozinc\ program into (essentially) \flatzinc, i.e., a program
where all calls are either solver-level constraint primitives, or defining
variables.
To achieve this, we define the semantics of \microzinc\ as a rewriting system
that produces \nanozinc\ calls. Each non-primitive call in a \nanozinc\ program
can then be replaced with the result of evaluating the corresponding \microzinc\
function.
A key element of this rewriting process, then, is the transformation of
functional expressions into relational form. For this to happen, the rewriting
process introduces fresh variables to represent intermediate expressions, and
keeps track of constraints added during the rewriting.
\begin{example}\label{ex:4-abs}
Consider the following (reasonably typical) \minizinc\ encoding for the
absolute value function:
\begin{mzn}
function var int: abs(var int: x) =
let {
var int: z;
constraint int_abs(x, z);
} in z;
\end{mzn}
When an occurrence of \mzninline{abs} is encountered, the call will be
replaced by a variable \mzninline{z}. But before that can occur, the variable
must be introduced, and the constraint \mzninline{int_abs(x, z)} is added to
the model, enforcing its value.
\end{example}
The function \mzninline{abs(x)} above is a total function: for every value of
\mzninline{x}, there is always exactly one valid value of \mzninline{z}. In
this case, the constraint introduced in the \mzninline{let} expression may be
enforced globally, and multiple occurrences of \mzninline{abs(x)} may be
replaced with the same \mzninline{z}. However, the introduced
\mzninline{int_abs} constraint is only needed so long as any \emph{other}
constraint refers to \mzninline{z}. If \mzninline{z} is unused in the rest of
the model, both \mzninline{z} and the \mzninline{int_abs} constraint can be
removed.
As we shall see in \cref{sec:4-simplification}, certain optimisations and
simplifications during the evaluation can lead to many expressions becoming
unused. It is therefore important to track which constraints were merely
introduced to define results of function calls. In order to track these
dependencies, \nanozinc\ attaches constraints that define a variable to the item
that introduces the variable.
\subsection{\glsentrytext{nanozinc}}\label{sec:4-nanozinc}
A \nanozinc\ model (\cref{fig:4-nzn-syntax}) consists of a topologically-ordered
list of definitions. Each definition binds a variable to the result of a call or
another variable, and it is associated with a list of identifiers of auxiliary
constraints. The \nanozinc\ model contains a special definition $\true$,
containing all ``root-context'' constraints of the model, i.e., those that have
to hold globally and are not just used to define an auxiliary variable. Only
root-context constraints (attached to $\true$) can effectively constrain the
overall problem. Constraints attached to definitions originate from function
calls, and since all functions are guaranteed to be total, attached constraints
can only define the function result.
\begin{example}\label{ex:4-absnzn}
Consider the \minizinc\ fragment\\\mzninline{constraint abs(x) > y;}\\ where
\mzninline{x} and \mzninline{y} have domain $[-10, 10]$. After rewriting all
definitions, the resulting \nanozinc\ program could look like this:
\begin{mzn}
x @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
y @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
z @$\mapsto$@ mkvar(-infinity,infinity) @$\sep$@ []
b0 @$\mapsto$@ int_abs(x, z) @$\sep$@ []
t @$\mapsto$@ z @$\sep$@ [b0]
b1 @$\mapsto$@ int_gt(t,y) @$\sep$@ []
true @$\mapsto$@ true @$\sep$@ [b1]
\end{mzn}
The variables \mzninline{x} and \mzninline{y} result in identifiers bound to
the special built-in \mzninline{mkvar}, which records the variable's domain in
its arguments. Evaluating the call \mzninline{abs(x)} introduces a new
variable \mzninline{z} (with unrestricted domain) and the \mzninline{int_abs}
constraint that is used to define \mzninline{z}, bound to a fresh identifier
\mzninline{b0}. A fresh identifier \mzninline{t} is introduced to capture both
the result \mzninline{z} as well as the list of defining constraints,
\mzninline{[b0]}. The top-level constraint \mzninline{abs(x) > y} is rewritten
into \mzninline{int_gt(t,y)} and bound to fresh identifier \mzninline{b1},
which is added to the root-context definition \mzninline{true}.
\end{example}
\subsection{Rewriting Rules}
The core of the proposed \minizinc\ architecture is an abstract machine that
performs the rewriting of \nanozinc\ items using the corresponding \microzinc\
function definitions. We can now formally define the rewriting rules for this
abstract machine.
The full set of rules appears in
\cref{fig:4-rewrite-to-nzn-calls,fig:4-rewrite-to-nzn-let,fig:4-rewrite-to-nzn-other}.
To simplify presentation, we assume that all rules that introduce new
identifiers into the \nanozinc\ program do so in a way that ensures those
identifiers are fresh (\ie\ not used in the rest of the \nanozinc\ program), by
suitable alpha renaming.
\begin{figure*}
\centering
\begin{prooftree}
\AxiomC{$\ptinline{F($p_1, \ldots, p_k$) = E;} \in \Prog$ where the $p_i$
are fresh}
\noLine\
\UnaryInfC{\Sem{$a_i$}{\Prog, \Env_{i-1}} $\Rightarrow~ \tuple{v_i,
\Env_i'}, \ \Env_0=\Env, \Env_i=\Env_i'\cup\{p_i\mapsto v_i\sep[]\}
\quad \forall
1 \leq
i \leq k$}
\noLine\
\UnaryInfC{\Sem{E}{\Prog, \Env_k}
$\Rightarrow ~ \tuple{v, \Env'}$}
\RightLabel{(Call)}
\UnaryInfC{\Sem{F($a_1, \ldots, a_k$)}{\Prog, \Env} $\Rightarrow$
$\tuple{v, \Env'}$}
\end{prooftree}
\begin{prooftree}
\AxiomC{$\ptinline{F} \in$ Builtins}
% \noLine
\AxiomC{\Sem{$a_i$}{\Prog, \Env} $\Rightarrow~ \tuple{v_i, \Env},
\forall
1 \leq
i \leq k$}
% $\ldots$, \Sem{$a_k$}{\Prog, \Env} $\Rightarrow~\tuple{\Ctx_k,v_k}$}
\RightLabel{(CallBuiltin)}
\BinaryInfC{\Sem{F($a_1, \ldots, a_k$)}{\Prog, \Env} $\Rightarrow$
$\tuple{ \mathit{eval}(\ptinline{F}(v_1,\ldots, v_k)), \Env}$}
\end{prooftree}
\begin{prooftree}
\AxiomC{$\ptinline{F($p_1, \ldots, p_k$);} \in \Prog$}
% \noLine
\AxiomC{\Sem{$a_i$}{\Prog, \Env_{i-1}} $\Rightarrow~ \tuple{v_i, \Env_i},
~\forall
1 \leq
i \leq k$}
% $\ldots$, \Sem{$a_k$}{\Prog, \Env} $\Rightarrow~\tuple{\Ctx_k,v_k}$}
\RightLabel{(CallPrimitive)}
\BinaryInfC{\Sem{F($a_1, \ldots, a_k$)}{\Prog, \Env_0} $\Rightarrow$
$\tuple{ x, \{ x \mapsto \ptinline{F}(v_1,\ldots, v_k) \sep [] \} \cup \Env_k}$}
\end{prooftree}
\caption{\label{fig:4-rewrite-to-nzn-calls} Rewriting rules for partial evaluation
of \microzinc\ calls to \nanozinc.}
\end{figure*}
The rules in \cref{fig:4-rewrite-to-nzn-calls} handle function calls.
The first rule (Call) evaluates a function call expression in the context of a
\microzinc\ program $\Prog$ and \nanozinc\ program $\Env$. The rule first
evaluates all actual parameter expressions $a_i$, creating new contexts where
the evaluation results are bound to the formal parameters $p_i$. It then
evaluates the function body $\ptinline{E}$ on this context, and returns the
result.
The (CallBuiltin) rule applies to ``built-in'' functions that can be evaluated
directly, such as arithmetic and Boolean operations on fixed values. The rule
simply returns the result of evaluating the built-in function on the evaluated
parameter values.
The (CallPrimitive) rule applies to non-built-in functions that are defined
without a function body. These are considered primitives, and as such simply
need to be transferred into the \nanozinc\ program. The rule evaluates all
actual parameters, and creates a new variable $x$ to store the result, which is
simply the function applied to the evaluated parameters.
\begin{figure*}
\centering
\begin{prooftree}
\AxiomC{\Sem{$\mathbf{I}$}{\Prog, \Env} $\Rightarrow (\Ctx, \Env')$}
\AxiomC{\Sem{$E$}{\Prog, \Env'} $\Rightarrow \tuple{v, \Env''}$}
\AxiomC{$x$ fresh}
\RightLabel{(LetC)}
\TrinaryInfC{\Sem{let \{ $\mathbf{I}$ \} in $E$}{\Prog, \Env} $\Rightarrow
\tuple{x, \{ x \mapsto v \sep \Ctx \} \cup \Env''}$}
\end{prooftree}
% \begin{prooftree}
% \AxiomC{\Sem{$E_1$}{\Prog, \Env} $\Rightarrow \Ctx_1, r_1$}
% \AxiomC{\Sem{$E_2$}{\Prog, \Env} $\Rightarrow \Ctx_2, r_2$}
% \RightLabel{(Rel)}
% \BinaryInfC{\Sem{$E_1 \bowtie E_2$}{\Prog, \Env} $\Rightarrow \Ctx_1 \wedge \Ctx_2 \wedge (r_1 \bowtie r_2)$}
% \end{prooftree}
\begin{prooftree}
\AxiomC{}
\RightLabel{(Item0)}
\UnaryInfC{\Sem{$\epsilon$}{\Prog, \Env} $\Rightarrow
(\ptinline{true}, \Env$)}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{$\mathbf{I}$}{\Prog, \Env\ \cup\ \{x \mapsto\ \texttt{mkvar()} \sep\ []\}} $\Rightarrow (\Ctx, \Env')$}
\RightLabel{(ItemT)}
\UnaryInfC{\Sem{$t:x, \mathbf{I}$}{\Prog, \Env} $\Rightarrow
(\Ctx, \Env')$}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{$E$}{\Prog, \Env} $\Rightarrow \tuple{v, \Env'}$}
\AxiomC{\Sem{$\mathbf{I}$}{\Prog, \Env' \cup\ \{x \mapsto\ v \sep\ [] \}} $\Rightarrow (\Ctx, \Env'')$}
\RightLabel{(ItemTE)}
\BinaryInfC{\Sem{$t:x = E, \mathbf{I}$}{\Prog, \Env} $\Rightarrow
(\Ctx, \Env'')$}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{$C$}{\Prog, \Env} $\Rightarrow \tuple{v, \Env'}$}
\AxiomC{\Sem{$\mathbf{I}$}{\Prog, \Env'} $\Rightarrow (\Ctx, \Env'')$}
\RightLabel{(ItemC)}
\BinaryInfC{\Sem{$\mbox{constraint~} C, \mathbf{I}$}{\Prog, \Env} $\Rightarrow
(\{v\}\cup\Ctx, \Env'')$}
\end{prooftree}
\caption{\label{fig:4-rewrite-to-nzn-let} Rewriting rules for partial evaluation
of \microzinc\ let expressions to \nanozinc.}
\end{figure*}
The rules in \cref{fig:4-rewrite-to-nzn-let} considers let expressions. The
(LetC) rule generates the constraints $\phi$ and \nanozinc\ program $\Env'$
defined by the let items \textbf{I}, then evaluates the body expression $E$ in
this new context. It finally binds the result $v$ to a fresh variable $x$,
collecting the constraints $\phi$ that arose from the let items: $x\mapsto
v\sep\phi$. The (ItemT) rule handles introduction of new variables by adding
them to the context. The (ItemTE) rule handles introduction of new variables
with a defining equation by evaluating them in the current context and adding a
definition to the context to evaluate the result of the items. The (Item0) rule
is the base case for a list of let items.
\begin{figure*}
\centering
\begin{prooftree}
\RightLabel{(IdC)}
\AxiomC{$x \in \langle ident \rangle$}
\AxiomC{$v \in \langle literal \rangle$}
\BinaryInfC{\Sem{$x$}{\Prog, \{x \mapsto\ v \sep\ []\} \cup\ \Env} $\Rightarrow \tuple{v, \{x \mapsto v \sep [] \} \cup \Env}$}
\end{prooftree}
\begin{prooftree}
\RightLabel{(IdX)}
\AxiomC{$x \in \langle ident \rangle$}
\AxiomC{$v$}
\AxiomC{$\phi$ otherwise}
\TrinaryInfC{\Sem{$x$}{\Prog, \{x \mapsto\ v \sep\ \phi\ \} \cup\ \Env} $\Rightarrow \tuple{x, \{x \mapsto v \sep \phi \} \cup \Env}$}
\end{prooftree}
\begin{prooftree}
\RightLabel{(Const)}
\AxiomC{$c$ constant}
\UnaryInfC{\Sem{c}{\Prog, \Env} $\Rightarrow \tuple{c, \Env}$}
\end{prooftree}
\begin{prooftree}
\RightLabel{(If$_T$)}
\AxiomC{\Sem{$C$}{\Prog, \Env} $\Rightarrow$ $\tuple{\ptinline{true}, \_}$}
\UnaryInfC{\Sem{if $C$ then $E_t$ else $E_e$ endif}{\Prog, \Env} $\Rightarrow$ \Sem{$E_t$}{\Prog, \Env}}
\end{prooftree}
\begin{prooftree}
\RightLabel{(If$_F$)}
\AxiomC{\Sem{$C$}{\Prog, \Env} $\Rightarrow$ $\tuple{\ptinline{false}, \_}$}
\UnaryInfC{\Sem{if $C$ then $E_t$ else $E_e$ endif}{\Prog, \Env} $\Rightarrow$ \Sem{$E_e$}{\Prog, \Env}}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{G}{\Prog,\Env} $\Rightarrow \tuple{ \ptinline{true}, \_}$}
\AxiomC{\Sem{$E$}{\Prog, \Env} $\Rightarrow \tuple {v, \Env'}$}
\RightLabel{(WhereT)}
\BinaryInfC{\Sem{$[E ~|~ \mbox{where~} G]$}{\Prog, \Env} $\Rightarrow
\tuple{[v], \Env'}$}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{G}{\Prog,\Env} $\Rightarrow \tuple{ \ptinline{false}, \_}$}
\RightLabel{(WhereF)}
\UnaryInfC{\Sem{$[E ~|~ \mbox{where~} G]$}{\Prog, \Env} $\Rightarrow
\tuple{[], \Env}$}
\end{prooftree}
\begin{prooftree}
\AxiomC{\Sem{$\mathit{PE}$}{\Prog,\Env} $\Rightarrow \tuple{S, \_}$}
\noLine\
\UnaryInfC{\Sem{$[E ~|~ \mathit{GE} \mbox{~where~} G]$}{\Prog, \Env\ \cup\ \{ x\mapsto\
v \sep\ []\}}}
\noLine\
\UnaryInfC{\hspace{8em} $\Rightarrow \tuple{A_v, \Env \cup \{x \mapsto v \sep []\} \cup \Env_v}, \forall
v \in S $}
\RightLabel{(ListG)}
\UnaryInfC{\Sem{$[E~|~ x \mbox{~in~} \mathit{PE}, \mathit{GE} \mbox{~where~} G]$}{\Prog, \Env} $\Rightarrow
\tuple{\mbox{concat} [ A_v ~|~ v \in S ], \Env \cup \bigcup_{v \in S} \Env_v}$}
\end{prooftree}
\caption{\label{fig:4-rewrite-to-nzn-other} Rewriting rules for partial evaluation
of other \microzinc\ expressions to \nanozinc.}
\end{figure*}
Finally, the rules in \cref{fig:4-rewrite-to-nzn-other} handle the evaluation of
variables, constants, conditionals and comprehensions. For completeness, we give
the details here.
The (IdX) and (IdC) rules look up a variable in the environment and return its
bound value (for constants) or the variable itself. The (Const) rule simply
returns the constant. The (If$_T$) and (If$_F$) rules evaluate the if condition
and then return the result of the then or else branch appropriately. The
(WhereT) rule evaluates the guard of a where comprehension and if true returns
the resulting expression in a list. The (WhereF) rule evaluates the guard and
when false returns an empty list. The (ListG) rule evaluates the iteration set
$PE$ to get its value $S$, which must be fixed. It then evaluates the expression
$E$ with all the different possible values $v \in S$ of the generator $x$,
collecting the additional \nanozinc\ definitions $\Env_v$. It returns the
concatenation of the resulting lists with all the additional \nanozinc\ items.
\subsection{Prototype Implementation}
Our prototype implementation of the \microzinc/\nanozinc\ framework consists of
the following components.
The \textbf{compiler} translates \minizinc\ into a \textbf{bytecode} encoding of
\microzinc. The compiler currently supports a significant subset of the full
\minizinc\ language, with the missing features (such as multi-dimensional arrays
and complex output statements) requiring additional engineering effort but no
new technology.
The \textbf{interpreter} evaluates \microzinc\ bytecode and \nanozinc\ programs
based on the rewrite system introduced in this section. It can read parameter
data from a file or via an API\@. The interpreter constructs the call to the
\mzninline{main} function, and then performs the rewriting into flat \nanozinc.
Memory management is implemented using reference counting, which lends itself
well to the optimisations discussed in the next section. The interpreter is
written in around 3.5kLOC of C++ to enable easy embedding into applications and
other programming languages.
The framework includes \textbf{interfaces} to multiple solvers. It can
pretty-print the \nanozinc\ code to standard \flatzinc, so that any solver
currently compatible with \minizinc\ can be used. Additionally, a direct C++ API
can be used to connect solvers directly, in order to enable incremental
solving, as described in \cref{sec:4-incremental}.
\textbf{Python bindings} to the framework are provided to enable easy
scripting.
The source code for the complete system will be made available under an open
source license.
\section{NanoZinc Simplification}\label{sec:4-simplification}
The previous section introduced the basic evaluation model of \nanozinc: each
call that has a corresponding \microzinc\ definition can be rewritten into a set
of calls by evaluating the \microzinc\ code. This section looks at further steps
that the system can perform in order to produce better, more compact \nanozinc\
code.
\subsection{Removing dead variables and constraints}
The most basic form of simplification is to identify variables that are not
used by any call, and remove them. This is correct because the semantics of
\nanozinc\ requires us to find values for all variables such that all
constraints are satisfied --- but if a variable does not occur in any
constraint, we can pick any value for it. The model without the variable is
therefore equisatisfiable with the original model.
Consider now the case where a variable in \nanozinc\ is only used in its own
auxiliary definitions (the \mzninline{@$\sep\phi$@} part).
\begin{example}\label{ex:4-absreif}
The following is a slight variation on the \minizinc\ fragment in
\cref{ex:4-absnzn}:
\begin{mzn}
constraint abs(x) > y \/ c;
constraint c;
\end{mzn}
The constraint \mzninline{abs(x) > y} is now used in a disjunction. This means
that instead of enforcing the constraint globally, the model needs to reason
about whether the constraint holds or not. This is called \gls{reification}
(we will come back to this in \cref{sec:4-cse}). Following the rewriting
rules, the generated \nanozinc\ code will look very similar to the code
generated in \cref{ex:4-absnzn}, but with an additional \mzninline{bool_or}
constraint for the disjunction, which uses the variable \mzninline{b1} that
was introduced for \mzninline{abs(x) > y}:
\begin{mzn}
c @$\mapsto$@ true @$\sep$@ []
x @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
y @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
b0 @$\mapsto$@ int_abs(x, z) @$\sep$@ []
b1 @$\mapsto$@ int_gt(t,y) @$\sep$@ []
z @$\mapsto$@ mkvar(-infinity,infinity) @$\sep$@ []
t @$\mapsto$@ z @$\sep$@ [b0]
b2 @$\mapsto$@ bool_or(b1,c) @$\sep$@ []
true @$\mapsto$@ true @$\sep$@ [b2,c]
\end{mzn}
Since \mzninline{c} is bound to \mzninline{true}, the disjunction
\mzninline{b2} trivially holds, independent of the value of \mzninline{b1}.
The constraint \mzninline{abs(x) > y} is therefore not required. However, the
rewriting has to choose a particular order in which arguments are evaluated,
so it is always possible that it already evaluated the left hand side before
``noticing'' that the disjunction is true.
If the system now simplifies the constraint \mzninline{b2} to \mzninline{true},
the identifier \mzninline{b1} will become unused, since it was only
referenced from the \mzninline{bool_or} call. Removing \mzninline{b1} leads
to \mzninline{t} not being used by any constraint, so its definition can be
removed. This in turn means that \mzninline{b0} is not referenced anywhere
and can be removed, and then \mzninline{z}. The resulting \nanozinc\ program
is much more compact:
\begin{mzn}
c @$\mapsto$@ true @$\sep$@ []
x @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
y @$\mapsto$@ mkvar(-10..10) @$\sep$@ []
true @$\mapsto$@ true @$\sep$@ []
\end{mzn}
We assume here that \mzninline{c}, \mzninline{x} and \mzninline{y} are used
in other parts of the model not shown here and therefore not removed.
\end{example}
Note how \mzninline{t} acts as a unique name for the result of the let
expression (and thus the function call) --- we could not simply have used
\mzninline{z}, because then \mzninline{b0} would have kept \mzninline{z} alive.
It is worth noting the difference between the constraints \mzninline{b0} and
\mzninline{b1} in this example: \mzninline{b0} is referenced from the auxiliary
constraint list of another variable. This means that it is a constraint that
defines the value of a variable, and it can be enforced globally (it must hold,
under all circumstances). On the other hand, \mzninline{b1} is referenced as an
argument to the \mzninline{bool_or} call. This means that the constraint does
not have to hold globally --- rather, we require the variable \mzninline{b1} to
reflect whether the constraint holds or not, so that the \mzninline{bool_or}
can implement the disjunction. These types of constraints are typically called
\emph{reified} in constraint modelling. So \nanozinc\ uses the same syntax for
reified and globally true constraints, allocating an identifier (such as
\mzninline{b0} or \mzninline{b1}) for any constraint. Whether the constraint is
reified or not depends on where that identifier appears.
\begin{example}
The importance of the use of auxiliary definitions when removing dead
variables and constraints is demonstrated in the following example:
\begin{mzn}
int n;
var int: x;
function var int: f(var int: x, int: i) = let {
var int: y;
constraint y = int_minus(x, i);
} in y;
constraint [f(x,i) | i in 1..n][1] == 10;
\end{mzn}
The rewriting of this instance will initially create the \mzninline{n}
elements for the array comprehension. Each element is evaluated as a new
variable \mzninline{y} and a constraint to ensure that \mzninline{y} is the
subtraction of \mzninline{x} and \mzninline{i}. Although \mzninline{n}
variables are created, only the first variable is constrained to be 10. All
other variables can thus be removed and the model will stay equisatisfiable.
In the \nanozinc\ version of this model, each of the subtraction constraints
will be added to the list of auxiliary constraints of its corresponding
\mzninline{y} variable. When, after evaluating the array access, all of those
variables except the first are detected to be unused, their removal triggers
the removal of the subtraction constraints.
For this example the current \minizinc\ compiler, version 2.3.2, will produce
a \flatzinc\ program that contains the variable \mzninline{x}, \mzninline{n-1}
variables \mzninline{y}, and \mzninline{n} constraints for the subtraction.
The first \mzninline{y} variable is directly replaced with the constant 10.
Using the \nanozinc\ system, only 1 variable, \mzninline{x}, and 1 constraint,
\mzninline{10 = int_minus(x,1);} is produced.
\end{example}
\paragraph{Implementation} The removal of unused identifiers is taken care of
by garbage collection in the interpreter. Since our prototype interpreter uses
reference counting, it already accurately tracks the liveness of each
identifier.
\subsection{Constraint propagation}
Another way that variables can become unused is if a constraint can be detected
to be true based on its semantics, and the known domains of the variables. For
example, consider the constraint \mzninline{3*x + 7*y > 8}, and assume that
both \mzninline{x} and \mzninline{y} cannot be smaller than 1. In this case, we
can determine that the constraint is always satisfied, and remove it from the
model without changing satisfiability. This, in turn, may result in
\mzninline{x} or \mzninline{y} becoming unused and also removed (including
their auxiliary definitions, if any).
This is a simple form of \gls{propagation}, the main inference method used in
constraint solvers \autocite{schulte-2008-propagation}. Constraint propagation,
in general, can not only detect when constraints are trivially true (or false),
but also tighten variable domains. For instance, given the constraint
\mzninline{x>y}, with initial domains \mzninline{x in 1..10, y in 1..10}, would
result in the domains being tightened to \mzninline{x in 2..10, y in 1..9}.
These tightened domains would be reflected in the corresponding
\mzninline{mkvar(...)} calls. Note, however, that this may now prevent us from
removing \mzninline{x} or \mzninline{y}: even if they later become unused, the
tightened domains may impose a constraint on their definition. For instance, if
\mzninline{x} is defined as \mzninline{abs(z)}, then any restriction on the
domain of \mzninline{x} constrains the possible values of \mzninline{z} through
the \mzninline{abs} function. We therefore need to track whether a variable
domain has been enforced through external constraints, or is the consequence of
its own definition. We omit the details of this mechanism for space reasons.
A further effect of constraint propagation is the replacement of constraints by
simpler versions. A constraint \mzninline{x=y+z}, where the domain of
\mzninline{z} has been tightened to the singleton \mzninline{0..0}, can be
rewritten to \mzninline{x=y}. Propagating the effect of one constraint can thus
trigger the propagation of other constraints, and constraint solvers typically
run all propagation rules or algorithms until they reach a fix-point.
The \nanozinc\ interpreter applies propagation rules for the most important
constraints, such as linear and Boolean constraints, which can lead to
significant reductions in generated code size. It has been shown previously that
applying full constraint propagation during compilation can be beneficial
\autocite{leo-2015-multipass}, in particular when the target solver requires
complicated reformulations (such as linearisation for MIP solvers
\autocite{belov-2016-linearisation}).
Returning to the example above, constraint propagation (and other model
simplifications) can result in \emph{equality constraints} \mzninline{x=y} being
introduced. For certain types of target solvers, in particular those based on
constraint programming, equality constraints can lead to exponentially larger
search trees if not detected and simplified. Equalities therefore present a
valuable opportunity for model simplification, but also pose some challenges.
Consider the simple case of \mzninline{x=y} where one of the variables, say
\mzninline{x}, was a top-level variable introduced by the modeller, and not
defined as the result of a function call. In that case, we can propagate the
equality constraint by replacing all occurrences of \mzninline{x} with
\mzninline{y}, and remove the definition of \mzninline{x}.
The more complicated case for \mzninline{x=y} is when both \mzninline{x} and
\mzninline{y} are the result of function calls, \eg\ from a model fragment such
as \mzninline{constraint f(a)=g(b)}. Let us assume that part of the
corresponding \nanozinc\ code looks like this:
\begin{mzn}
x @$\mapsto$@ mkvar() @$\sep$@ [];
y @$\mapsto$@ mkvar() @$\sep$@ [];
b0 @$\mapsto$@ f_rel(a,x) @$\sep$@ [];
b1 @$\mapsto$@ g_rel(b,y) @$\sep$@ [];
t0 @$\mapsto$@ x @$\sep$@ [b0];
t1 @$\mapsto$@ y @$\sep$@ [b1];
b2 @$\mapsto$@ int_eq(t0,t1) @$\sep$@ [];
true @$\mapsto$@ true @$\sep$@ [b2];
\end{mzn}
In this case, simply removing either \mzninline{t0} or \mzninline{t1} would not
be correct, since that would trigger the removal of the corresponding definition
\mzninline{b0} or \mzninline{b1}, leaving nothing to be substituted. Rather, the
interpreter needs to pick one of the two definitions and \emph{promote} it to
top-level status. Let us assume that the interpreter decides to replace
\mzninline{t1} by \mzninline{t0}. It follows the definition of \mzninline{t1} to
find \mzninline{y}, which is then globally replaced by \mzninline{t0}, and its
definition is removed. The interpreter moves all auxiliary constraints from
\mzninline{t1} into the list of auxiliary constraints of the top-level
\mzninline{true} item, and then removes the \mzninline{int_eq} constraint and
\mzninline{t1}. The resulting \nanozinc\ looks like this:
\begin{mzn}
x @$\mapsto$@ mkvar() @$\sep$@ [];
b0 @$\mapsto$@ f_rel(a,x) @$\sep$@ [];
b1 @$\mapsto$@ g_rel(b,t0) @$\sep$@ [];
t0 @$\mapsto$@ x @$\sep$@ [b0];
true @$\mapsto$@ true @$\sep$@ [b1];
\end{mzn}
The process of equality propagation is similar to unification in logic
programming \autocite{warren-1983-wam}. However, since equations in \minizinc\
always concern scalar variables rather than terms, we can restrict ourselves to
this simple substitution scheme and leave the further checking of the resulting
equalities to the target constraint solver.
\paragraph{Implementation}
Rewriting a function call that has a \microzinc\ definition and rewriting a
constraint due to propagation are very similar. The interpreter therefore simply
interleaves both forms of rewriting. Efficient constraint propagation engines
``wake up'' a constraint only when one of its variables has received an update
(i.e., when its domain has been shrunk). To enable this, the interpreter needs
to keep a data structure linking variables to the constraints where they appear
(in addition to the reverse links from calls to the variables in their
arguments). These links do not take part in the reference counting.
\subsection{Delayed Rewriting}
Adding constraint propagation and simplification into the rewriting system means
that the system becomes non-confluent, and some orders of execution may produce
``better'', i.e., more compact or more efficient, \nanozinc.
\begin{example}
The following example is similar to code found in the \minizinc\ libraries of
MIP solvers.
\begin{mzn}
predicate lq_zero_if_b(var int: x, var bool: b) =
x <= ub(x)*(1-b);
\end{mzn}
This predicate expresses the constraint \mzninline{b -> x<=0}, using a
well-known method called ``big-M transformation''. The expression
\mzninline{ub(x)} returns a valid upper bound for \mzninline{x}, \ie\ a fixed
value known to be greater than or equal to \mzninline{x}. This could be the
initial upper bound \mzninline{x} was declared with, or the current value in
the corresponding \nanozinc\ \mzninline{mkvar} call. If \mzninline{b} takes
the value 0, the expression \mzninline{ub(x)*(1-b)} is equal to
\mzninline{ub(x)}, and the constraint \mzninline{x <= ub(x)} holds trivially.
If \mzninline{b} takes the value 1, \mzninline{ub(x)*(1-b)} is equal to 0,
enforcing the constraint \mzninline{x <= 0}.
\end{example}
For MIP solvers, it is quite important to enforce \emph{tight} bounds in order
to improve efficiency and sometimes even numerical stability. It would therefore
be useful to rewrite the \mzninline{lq_zero_if_b} predicate only after all the
constraint propagation has been performed, in order to take advantage of the
tightest possible bounds. On the other hand, evaluating a predicate may also
\emph{impose} new bounds on variables, so it is not always clear which order of
evaluation is best.
In our \microzinc/\nanozinc\ system, we allow predicates and functions to be
\emph{annotated} as potential candidates for \emph{delayed rewriting}. Any
annotated constraint is handled by the (CallPrimitive) rule rather than the
(Call) rule, which means that it is simply added as a call into the \nanozinc\
code, without evaluating its body. When propagation reaches a fix-point, all
annotations are removed from the current \nanozinc\ program, and evaluation
resumes with the predicate bodies.
This crucial optimisation enables rewriting in multiple \emph{phases}. For
example, a model targeted at a MIP solver is rewritten into Boolean and reified
constraints, whose definitions are annotated to be delayed. The \nanozinc\ model
can then be fully simplified by constraint propagation, before the Boolean and
reified constraints are rewritten into lower-level linear primitives suitable
for MIP\@.
\subsection{Common Subexpression Elimination}\label{sec:4-cse}
The simplifications introduced above remove definitions from the model when we
can detect that they are no longer needed. In some cases, it is even possible to
not generate definitions in the first place through the use of common
subexpression elimination (CSE). This simplification is a well understood
technique that originates from compiler optimisation \autocite{cocke-1970-cse}
and has proven to be very effective in discrete optimisation
\autocite{marinov-2005-sat-optimisations, araya-2008-cse-numcsp}, including
during the evaluation of constraint modelling languages such as
\minizinc\ \autocite{rendl-2009-enhanced-tailoring}.
For instance, in the constraint\\
\mzninline{(abs(x)*2 >= 20) \/ (abs(x)+5 >= 15)}\\
the expression \mzninline{abs(x)} is evaluated twice. Since functions in
\microzinc\ are guaranteed to be pure and total, any expressions that are
syntactically equal can be detected by a static compiler analysis before
evaluation even begins, and hoisted into a let expression:
\begin{mzn}
constraint let { var int: y = abs(x) } in
(y*2 >= 20) \/ (y+5 >= 15)
\end{mzn}
%
However, some expressions only become syntactically equal during evaluation, as
in the following example.
%
\begin{example}
Consider the fragment:
\begin{mzn}
function var float: pythagoras(var float: a, var float: b) =
let {
var float: x = pow(a, 2);
var float: y = pow(b, 2);
} in sqrt(x + y);
constraint pythagoras(i, i) >= 5;
\end{mzn}
Although the expressions \mzninline{pow(a, 2)} and \mzninline{pow(b, 2)} are
not syntactically equal, the function call \mzninline{pythagoras(i,i)} using
the same variable for \mzninline{a} and \mzninline{b} makes them equivalent.
A straightforward approach to ensure that the same instantiation of a
function is only evaluated once is through memorisation. After the evaluation
of a \microzinc\ call, the instantiated call and its result are stored in a
lookup table, the CSE table. Before a call is evaluated the CSE table is
consulted. If the call is found, then the accompanying result is used;
otherwise, the evaluation proceeds as normal.
In our example, the evaluation of \mzninline{pythagoras(i, i)} would proceed
as normal to evaluate \mzninline{x = pow(i, 2)}. However, the expression
defining \mzninline{y}, \mzninline{pow(i, 2)}, will be found in the CSE table
and replaced by the earlier stored result: \mzninline{y = x}.
\end{example}
It is worth noting that the CSE approach based on memorisation also covers the
static example above, where the compiler can enforce sharing of common
subexpressions before evaluation begins. However, since the static analysis is
cheap and can potentially avoid many dynamic table lookups, it is valuable to
combine both approaches. The static approach can be further improved by
\emph{inlining} function calls, since that may expose more calls as being
syntactically equal.
\paragraph{Reification}
Many constraint modelling languages, including \minizinc, not only enable
constraints to be globally enforced, but typically also support constraints to
be \emph{reified} into a Boolean variable. Reification means that a variable
\mzninline{b} is constrained to be true if and only if a corresponding
constraint \mzninline{c(...)} holds. We have already seen reification in
\cref{ex:4-absreif}: the truth of constraint \mzninline{abs(x) > y} was
bound to a Boolean variable \mzninline{b1}, which was then used in a
disjunction. We say that the same constraint can be used in \emph{root context}
as well as in a \emph{reified context}. In \minizinc, almost all constraints
can be used in both contexts. However, reified constraints are often defined in
the library in terms of complicated decompositions into simpler constraints, or
require specialised algorithms in the target solvers. In either case, it can be
very beneficial for the efficiency of the generated \nanozinc\ program if we can
detect that a reified constraint is in fact not required.
If a constraint is present in the root context, it means that it must hold
globally. If the same constraint is used in a reified context, it can therefore
be replaced with the constant \mzninline{true}, avoiding the need for
reification (or in fact any evaluation). We can harness CSE to store the
evaluation context when a constraint is added, and detect when the same
constraint is used in both contexts. Whenever a lookup in the CSE table is
successful, action can be taken depending on both the current and stored
evaluation context. If the stored expression was in root context, the constant
\mzninline{true} can be used, independent of the current context. If the stored
expression was in reified context and the current context is reified, the
stored result variable can be used. If the stored expression was in reified
context and the current context is root context, then the previous result can
be replaced by the constant \mzninline{true} (and all its dependencies removed)
and the evaluation will proceed as normal with the root context constraint.
\begin{example}
Consider the fragment:
\begin{mzn}
predicate p(var int: x, var int: y) = q(x) /\ r(y);
constraint b0 <-> q(x);
constraint b1 <-> t(x,y);
constraint b1 <-> p(x,y);
\end{mzn}
If we process the top-level constraints in order we create a reified call to
\mzninline{q(x)} for the original call. Suppose processing the second
constraint we discover \mzninline{t(x,y)} is \mzninline{true}, setting
\mzninline{b1}. When we process \mzninline{q(x)} in the call
\mzninline{t(x,y)} we find it is the root context. So CSE needs to set
\mzninline{b0} to \mzninline{true}.
\end{example}
\minizinc\ distinguishes not only between root and reified contexts, but in
addition recognises constraints in \emph{positive} contexts, also known as
\emph{half-reified} \autocite{feydy-2011-half-reif}. We omit the details here
for brevity, but the CSE approach generalises to this extension.
\section{Experiments}\label{sec:4-experiments}
We have created a prototype implementation of the architecture presented in the
preceding sections. It consists of a compiler from \minizinc\ to \microzinc, and
an incremental \microzinc\ interpreter producing \nanozinc. The system supports
a significant subset of the full \minizinc\ language; notable features that are
missing are support for set and float variables, option types, and compilation
of model output expressions and annotations. We will release our implementation
under an open-source license and can make it available to the reviewers upon
request.
The implementation is not optimised for performance yet, but was created as a
faithful implementation of the developed concepts, in order to evaluate their
suitability and provide a solid baseline for future improvements. In the
following we present experimental results on basic flattening performance as
well as incremental flattening and solving that demonstrate the efficiency
gains that are possible thanks to the new architecture.
\subsection{Basic Flattening}
As a first experiment, we selected 20 models from the annual \minizinc\
challenge and compiled 5 instances of each model to \flatzinc, using the current
\minizinc\ release version 2.4.3 and the new prototype system. In both cases we
use the standard \minizinc\ library of global constraints (i.e., we decompose
those constraints rather than using solver built-ins, in order to stress-test
the flattening). We measured pure flattening time, i.e., without time required
to parse and typecheck in version 2.4.3, and without time required for
compilation to \microzinc\ in the new system (compilation is usually very fast).
Times are averages of 10 runs.\footnote{All models obtained from
\url{https://github.com/minizinc/minizinc-benchmarks}:
\texttt{\justify{}accap, amaze, city-position, community-detection,
depot-placement, freepizza, groupsplitter, kidney-exchange, median-string,
multi-knapsack, nonogram, nside, problem, rcpsp-wet, road-cons, roster,
stack-cuttingstock, steelmillslab, train, triangular, zephyrus}.}
\Cref{sfig:4-compareruntime} compares the flattening time for each of the 100
instances. Points below the line indicate that the new system is faster. On
average, the new system achieves a speed-up of $2.3$, with very few instances
not achieving any speedup. In terms of memory performance
(\cref{sfig:4-comparemem}), version 2.4.3 can sometimes still outperform the new
prototype. We have identified that the main memory bottlenecks are our currently
unoptimised implementations of CSE lookup tables and argument vectors.
These are very encouraging results, given that we are comparing a largely
unoptimised prototype to a mature piece of software.
\begin{figure}[t]
\centering
\begin{subfigure}[b]{.48\columnwidth}
\centering
\includegraphics[width=\columnwidth]{assets/img/4_compare_runtime}
\caption{\label{sfig:4-compareruntime}flattening run time (ms)}
\end{subfigure}%
\hspace{0.04\columnwidth}%
\begin{subfigure}[b]{.48\columnwidth}
\centering
\includegraphics[width=\columnwidth]{assets/img/4_compare_memory}
\caption{\label{sfig:4-comparemem}flattening memory (MB)}
\end{subfigure}
\caption{\label{fig:4-runtime}Performance on flattening 100 MiniZinc Challenge
instances. \minizinc\ 2.4.3 (x-axis) vs new architecture (y-axis), log-log
plot. Dots below the line indicate the new system is better.}
\end{figure}