• No results found

Generating efficient code for search in Constraint Programming

N/A
N/A
Protected

Academic year: 2021

Share "Generating efficient code for search in Constraint Programming"

Copied!
11
0
0

Bezig met laden.... (Bekijk nu de volledige tekst)

Hele tekst

(1)

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

(2)

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)

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.

(4)

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:

(5)

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:

(6)

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.

(7)

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.

(8)

, 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.

(9)

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-

(10)

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

(11)

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.

Referenties

GERELATEERDE DOCUMENTEN

This smaller retrograde population only showed up after a careful analysis of the line-of-sight ve- locity distribution (LOSVD) derived from absorption-line spectra at a number of

Binding of apoE-rich high density lipoprotein particles by saturable sites on human blood platelets inhibits agonist- induced platelet aggregation.. The effect of blood

\selectVersion, eqexam will perform modular arithmetic on the number of available versions of a problem, in this way each problem will be properly posed; consequently, when we

The discussions are based on five lines of inquiry: The authority of the book as an object, how it is displayed and the symbolic capital it has; the authority of the reader and

To improve the quality of English in the generated texts training the larger (1.5B) GPT-2 model is likely to be beneficial. Additionally, more quantitative

We further utilize NaBSA and HBSA aqueous solutions to induce voltage signals in graphene and show that adhesion of BSA − ions to graphene/PET interface is so strong that the ions

Notwithstanding the relative indifference toward it, intel- lectual history and what I will suggest is its necessary complement, compara- tive intellectual history, constitute an

1p 12 „ What does John Humphrys make clear about Barnardo’s in paragraph 6. A It can no longer motivate people to support