Generating efficient code for search in Constraint Programming
Pieter Wuille1 and Tom Schrijvers2
1 Department of Computer Science, K.U.Leuven, Belgium
2 Department of Applied Mathematics and Computer Science, UGent, Belgium
Abstract. Within the domain of Constraint Programming (CP), much work exists around declarative specification of problem. However, hard CP problem often require a custom search algorithm, which is typically still written in an imperative way.
We present a declarative way for modelling search — using search com- binators — and an implementation in Haslell for generating efficient C++
code from such a specification. This is an illustration of large stacks of monad transformers, and monad-state dependent memoization.
1 Introduction
uitwerken
Constraint programming, modeling of problems: many declarative efforts TODO (refs) This is about modeling of search, typically either using one of many pre- defined search heuristics, or ad-hoc implemented in an imperative way.
We propose a declarative middle ground: a few base searches that can be combined without limit.
We provide an implementation in haskell, based on stacked monad trans- formers. This is an interesting application of dozens of monad transformers.
2 Language
uitwerken Zinc-like syntax, functions taking searches and returning composed searches variables can refer to problem variables. TODO
Llow-level enough that restarting search, lds, branch-and-bound can be ex- pressed high-level enough to abstract away from iterations, updating of statistics, . . .
2.1 Design
Introductory example nice example explanation of used combinators
TODO TODO Available combinators full list of available base combinators, and how
the earlier ones in the examples are expressible in terms of these
TODO
Exhaustiveness One particular run-time property of a search is its exhaus- tiveness: a flag that marks whether a search was exhaustive: whether its failure was due to a combinator deciding to switch away from it, or due to no solutions possible anymore. Certain combinators may use this information coming from their subsearches.
2.2 Semantics
At the lowest level, search is performed by repeatedly popping search nodes from a queue, processing them, and possibly pushing new nodes to the queue. We use a stack-based terminology here, but the underlying queue could as well be a FIFO or some other queue structure.
Node processing loop IJCAI diagram
TODO During the main processing loop, nodes are activated one by one. Doing so involves going through a series of hooks. There are several ones, each correspond- ing to a separate stage of processing a node. Initially, the body hook of the top combinator is activated. Typically, this will pass processing to one or more of its subcombinators. Eventually, the body hook of one of the base searches is reached, which restarts processing the chain, but now with the add hook. Similarly, add will eventually call try, and try will call result.
The purpose of these hooks is to allow combinators to modify the behaviour of generated code, by letting them insert or modify statements at different stages of processing.
Hooks
body In this stage, a check is done to verify whether it should be processed at all. Each combinator is allowed to introduce checks, and optionally stop the processing, by diverting the call to fail
add When this stage is reached, the node will definitely be processed. This hook is mainly used to add additional contraints to the problem.
try During this stage, checks are done to see whether branching on the node is required, and if so, and eventually either branch, fail, or result.
result This stage is reached when a solution has been found.
other hooks:
init Initialization of a new node.
fail Called whenever processing a node stopped.
3 Implementation
uitwerkenEverything is implemented as a haskell program that generates C++
TODO code, interacting with FlatZinc with Gecode backend, to do the actual solving.
3.1 C++ Abstract Syntax Tree
The lowest layer in the code generator is a C++ AST, of which part of the definition is given here:
data Stmt = Nop | Expr := Expr
| IfThenElse Expr Stmt Stmt | Stmt ; Stmt
| Call String [Expr ] | While Expr Stmt
| ...
A number of convenient abbreviations facilitate building this AST, e.g., s1# ms2 = liftM (s1 ;) ms2
In our modular code generation approach, AST fragments from different sources are combined. This often leads to ugly (non-idiomatic) code. To make the resulting C++source code readable, we have implemented a simplifier that is invoked by the pretty printer.
3.2 The Code Generator
The C++AST for a search heuristic is produced by a code generator:
data Gen m = Gen {initG :: m Stmt , bodyG:: m Stmt , addG :: m Stmt , tryG :: m Stmt , resultG:: m Stmt , failG :: m Stmt }
A code generator consists of a number of hooks, discussed previously in Sec- tion ??, that produce the corresponding AST fragments. Code generation may involve side effects; hence Gen is parameterized in a monad m.
These fragments are combined as follows.
gen :: Monad m ⇒ Gen m → m Stmt gen e = do init ← initG e
try ← tryGe body ← bodyGe
return $ do declarations
; init
; try
; While (queueNotEmpty) body
The full implementation will generate the necessary boiletplate code – func- tions and setup of data structures – then create an initial node through initG, call the tryG hook on this one, which will most likely cause branches of it to be added to the queue, after passing through the try stage. After this, a loop is generated that continuously pops nodes from the queue, and runs the code generated by the bodyG hook.
3.3 Code Generation Mixins
Instead of writing a monolithic code generator for every different search heuristic, we modularly compose new heuristics from one or more “components”. Our code generator components, are (functional) mixins [1]:
type Mixin a = a → a
type MGen m = Mixin (Gen m)
There are two kinds of mixin components: base components that are self- contained, and advice components that extend or modify one or more other components.
Base Component The main example of a base component is enumeration strat- egy baseM:
baseM :: Monad m ⇒ MGen m baseM this =
Gen {initG = return Nop , bodyG = addGthis
, addG = constrain# tryGthis , tryG = do fail ← failGthis ret ← resultGthis
let succ = IfThenElse isSolved ret doBranch return (IfThenElse isFailed fail succ) , resultG= return Nop
, failG = return Nop }
We have omitted details from the above code related to posting constraints (constrain), checking the solver status (isSolved or isFailed ) and branching (doBranch). The details of these operations depend on the particular constraint solver involved (e.g. finite domain, linear programming, . . . ); here we focus only on the search heuristics, which are orthogonal to those details.
As we can see the base component is parametrized by this, the overall search heuristic. The simplest form of search heuristic is obtained by applying the fixpoint combinator to a base component:
fix :: Mixin a → a fix m = m (fix m) search1 :: Gen Identity search1 = fix baseM
Advice Component The mixin mechanism allows us to plug in additional advice components before applying the fixpoint combinator. This way we can modify the base component’s behavior.
Consider a simple example of an advice combinator that prints solutions:
printM:: Monad m ⇒ MGen m
printM super = super {resultG = printSolution# resultGsuper }
where printSolution consists of the necessary solver-specific code to access and print the solution. A code generator is obtained through mixin composition, simply using (◦):
search2 :: Gen Identity
search2 = fix (printM ◦ baseM)
3.4 Monadic Components
In the components we have seen so far, the monad type parameter m has not been used. It does become essential when we turn to more complex components such as the binary conjunction andM.
type Mixin2a = a → a → a
andM:: MonadReader ⇒ Mixin2(Gen m) andM g1 g2 = Gen {...
}
Here the code dispatches to either the left (g1 ) or the right (g2 ) branch, depend- ing on an updateable state. Initially the code generator starts in the left state, but upon producing a result it switches to the right state.
Effect Encapsulation So, far we’ve always parametrized MGen with m, a monad type parameter. The reason is that we will allow each combinator to choose its own monad transformers. For example, a sequence combinatorandNmay want to keep a state during code generation about which stage it is in. This could be TODO accomplished by letting it run a StateT-transformed code generator monad.
data Search = ∀t2 .FMonadT t2 ⇒
Search {mkeval :: ∀m t1 .(Monad m, FMonadT t1 ) ⇒ MGen ((t1 B t2 ) m) , run :: ∀m x . Monad m ⇒ t2 m x → m x }
Instead of the earlier MGen m functions, we will use these Search structures.
They contain an existentially quantified monad transformer t2 , corresponding to the transformers used by the combinator. The mkeval field contains the wrapped MGen m function, but for a t2 -transformed and possibly further t1 -transformed type and base monad m. The run field contains the code to remove the t2 layer from the stack. Through the use of the existential type, we can hide which transformer is used by a combinator, while still being able to retrieve its result using the corresponding run code.
schema runL uitleggenTo effectively run combinators, simply extracting
TODO TODO the code resulting from the bodyE hook of the top of the stack, and plugging in
IdT as t1 , and Id as m, we can use:
generate :: Search → Stmt generate s =
case s of
Search {mkeval = mkeval , run = run } → let eval = fix mkeval
codeM = gen eval
in runIdentity ◦ run ◦ runIdT ◦ runL $ codeM
This code will first apply the fixpoint computation, passing the result back into itself, as explained earlier. After that, gen is called to get the real code-generating monad action. it extracts the knot-tied bodyG hook, runL flattens the trans- former combination (t1 B t2 ) m into t1 (t2 m), runIdT removes t1 (forcing it to be IdT ), run removes t2 (whatever that t2 was), and runIdentity finally runs m (forcing it to be Identity).
We can now write a function to lift MGen m to Search:
liftMkEval :: (∀m.Monad m ⇒ MGen m) → Search → Search liftMkEval f super =
case super of
Search {mkeval = mkeval , run = run } → Search {mkeval = f ◦ mkeval
, run = run }
Using this function, it is possible to write a search transformer for printing results:
printS:: Search → Search printS = liftMkEval printM
Note that liftMkEval only works for transformations that can be written as a simple function from Gen m to Gen m. More complex transformers, especially those that require their own monad transformers, or those combining multiple searches together, will need a custom Search → Search function.
simplified and combinator TODO
4 Memoization and Inlining
Experimental evaluation indicates that that several component hooks in a com- plex search heuristic are called frequently. explain how/why add counts in tom evaluation section to back up these claims This is a problem 1) for the tom code generation — which needs to generate the corresponding code over and over again — and 2) for the generated program which contains much redundant code. Both significantly impact the compilation time (in Haskell and in C++);
in addition, an overly large binary executable may aversely affect the cache and ultimately the running time.
4.1 Basic Memoization
A well-known approach that avoids the first problem, repeatedly computing the same result, is memoization. Actually, we are lucky, because, as Brown and Cook [2] have shown, memoization is quite easy to add as a monadic mixin component.
Memoization is a side effect for which we define a custom monad transformer:
newtype MT m a = MT {runMT:: StateT Table m a } deriving (Monad , MonadTrans)
runMemoT :: Monad m ⇒ MT m a → m (a, Table) runMemoT m = runStateT (runMT m) initMemoState
which is essentially a state transformer that maintains a table from Keys to Stmt s. For now we use Strings as Keys.
newtype Key = String
newtype Table = Map Key Stmt initMemoState = Map.empty
We capture the two essential operations of MT in a type class, which allows us to lift the operations through other monad transformers.3
class Monad m ⇒ MM m where get M :: String → m (Maybe Stmt) put M :: String → Stmt → m ()
instance Monad m ⇒ MM (MT m) where ...
instance (MM m, MonadTrans t ) ⇒ MM (t m) where ...
These operations are used in an auxiliary mixin function:
memo :: MM m ⇒ String → Mixin (m Stmt ) memo s m = do stm ← get M s
case stm of
Nothing → do code ← m put M s code return code Just code → return code which is used by the advice component:
memoM :: MM m ⇒ MGen m
memoM super = super {initG = memo "init" (initG super ) , bodyG = memo "body" (bodyG super ) , addG = memo "add" (addG super ) , tryG = memo "try" (tryG super )
3 For lack of space we omit the straightforward instance implementations.
, resultG= memo "result" (resultG super ) , failG = memo "fail" (failG super )}
which allows us to define, e.g., a memoized variant of printS. printS = liftMkEval (memoM◦ printM)
4.2 Monadic Memoization
Unfortunately, it is not quite this simple. The behavior of combinator hooks may depend on internal updateable state. The above memoization does not take this state dependency into account.
In order to solve this issue, we must expose the components’ state to the memoizer. This is done in two steps. First, MT keeps a context in addition to the memoization table, and provides access to it through the MM type class.
Second — for the specific case of a StateT s with s an instance of Showable — an alternative implementation (MemoStateT ) which updates the context in the MT layer below it, is provided.
To implement this, the Table type is extended:
type MemoContext = IntMap String
type Key = (MemoContext , String) data Table =
Table { context :: MemoContext , memoMap :: Map Key Stmt }
initMemoState = Table {context = IntMap.empty , memoMap = Map.empty }
MemoContext is represented as a map from integers to strings. The integers are identifiers assigned to the monad transformer layers that have context, and the strings are serialized versions of the contextual data inside those layers (using show ).
The MM type class is extended to support modifying the context information.
setContext and clearContext modify the context itself, while getCtxSize returns the size of the context.
class Monad m ⇒ MM m where ...
setCtx :: Int → String → m () clearCtx :: Int → m ()
getCtxSize :: m Int
Finally, MST is introduced. It will contain a wrapped ReaderT and StateT - transformed monad. The state will be stored in the StateT , while the ReaderT is used to give access to the identifier of the layer.
newtype MST s m a = MST {runMST:: ReaderT Int (StateT s m) a } deriving (Monad )
For convenience, MST is made an instance of MonadState, so switching from StateT to MST does not require any changes to the code interacting with it.
When running a MST transformer, the size of the context table is used as the id of that transformer layer. The runtime state itself is stored inside the wrapped StateT layer, while a serialized representation (using show ) is stored in the context of the underlying MT.
instance (Show s, MM m) ⇒ MonadState s (MST s m) where get = MST get
put s = MST $ do n ← ask
setCtx n (show s) put s
rStateT :: (MM m, Show s) ⇒ s → MST s m a → m a rStateT s m =
do depth ← getCtxSize
let action = runReaderT (runMST m) depth setCtx depth (show s)
result ← evalStateT action s clearCtx depth
return result
4.3 Inlining
By using the memoization system described in the previous section, the repeated execution of certain hooks is avoided. The next step is also preventing the re- peated code in the output. This is easy with all the established machinery.
Instead of returning the actual code, a function call is returned. The function getFnName — whose implementation is not given here — gives a unique name for a (C++) function with the given body.
getFnName :: Stmt → String
memo :: MM m ⇒ String → Mixin (m Stmt ) memo s m = do stm ← get M s
code ← case stm of Nothing → do code ← m
put M s code return code Just code → return code let name = getFnName code return (Call name [ ])
To extract both the body and the actual function bodies, the following generate function is used. By introducing runMemoT in the chain of evalua-
tion functions, the types change, and the result will be of type (Stmt , Table), since that is returned by runMemoT .
type FunctionDef = (String, Stmt ) toFunctionDef :: Stmt → FunctionDef toFunctionDef stm = (getFnName stm, stm) generate :: Search → (Stmt , [FunctionDef ]) generate s =
case s of
Search {mkeval = mkeval , run = run } → let eval = fix mkeval
codeM = gen eval
memoM = run ◦ runIdT ◦ runL $ codeM (code, state) = runIdentity $ runMemoT memoM
in (code, map toFunctionDef ◦ Map.elems $ memoMap state) Notice that using this approach, different hooks that somehow still generate identical code, are still combined into one single function. Since this is only detected after generating it again, this does not improve the efficiency of the code generator.
The final part, turning the Stmt s into C++source files, is trivial and omitted.
With this change, every single memoized body of code is now turned into a separate function. Although this was the intent, it is not entirely satisfactory.
Many memoized functions are only called once, or only contain a single line of code. One can either leave the choice for inlining them to the C++ compiler, or an additional pass is possible, where calls to small or rarely called functions are again replaced by their body, recursively.
5 Evaluation
differences with real implementation TODO
6 Related and Future Work
The work presented here about memoization builds upon that by Brown and Cook [2]. Using the example of the fibonacci function, they introduce memo- ization through open recursion and monad actions. Although mentioning the problem, they do not give a solution for the problem of memoizing monadic actions that depend on a state.
Though far less general than the implementation they give, we solve this problem by introducing a mutable context in the memoization transformer, and providing alternative implementations for context-introducing monad transform- ers.
A related approach is observable sharing [?], where one tries to detect several identical calls that exist in the code generator, and internally replace them by
references to a single instance. It requires either non-standard features from the language, or unsafePerformIO -based techniques, since sharing is normally not observable from within the language. Furthermore, it cannot detect different calls that still result in the same code, and is hard to integrate with code-generating monadic actions, which are necessary if the code generators require a mutable state.
7 Conclusions
Acknowledgments
References
1. Gilad Bracha and William R. Cook. Mixin-based inheritance. In Proc. of ACM Conf.
on Object-Oriented Programming, Systems, Languages and Applications (OOP- SLA), pages 303–311, 1990.
2. Daniel Brown and William R. Cook. Function inheritance: Monadic memoization mixins. In Brazilian Symposium on Programming Languages (SBLP), 2009.