Factors believed to be impacting IC performance

The following is a list of factors which are believed to be impacting the performance of the IC solver (as of ECLiPSe 5.5) along with how they're expected to be addressed in the rewrite (intended for ECLiPSe 5.6).  Note that these issues (and the rewrite) focus on linear constraints: these are by far the most common kind of constraint, and thus the most important to make fast (and any improvements will have the most impact).

It would be undesirable to implement and evaluate all of the alternatives discussed here, since it would be a waste of manpower to do this for alternatives we have reason to believe are likely to be worse, or would be costly to implement for little expected benefit.  Such alternatives are listed for completeness, and to explain why we think they're not worth trying.  Also, it is not expected to be possible to implement and properly evaluate even all the "worthwhile" alternatives before the 5.6 release.

Processor rounding modes
The use of processor rounding modes for interval arithmetic (rather than conservatively rounding all results) was implemented for the 5.5 release. It would have been quite difficult to maintain both the new and old approaches at the same time, and no formal evaluation of the impact of this change has been done, but it seems fairly clear that using processor rounding modes is faster and more accurate, and it allowed the simplification of a fair bit of code.  The trade-off is that a small amount of code needed to be written for each combination of operating system and processor.
(Is there still some work to be done on simplifying other code? E.g. linearize or something?)
set_var_type
The primitive IC constraints that appear in code after compile-time transformation typically assume that all variables are already IC variables, and that any variables required to be integral have already been constrained to be integral.  This results in lots of calls to set_var_type/2 in transformed code, most of which would be expected to be redundant, since most variables appearing in constraints will already have appeared in other constraints, and hence don't need to be "set up".  It ought to be more efficient to handle the cases where this is not the case in the C code which sets up a constraint (at least for constraints which have C code to set them up... :) - without using exceptions if possible (I can't see why they'd be needed anyway).
sync_ic_bounds & attrs vs. vars
Having constraints keep references to its variables' attributes rather than the variables themselves seemed like a good idea during the initial IC implementation, since it saved dereferencing the variables each time the constraint was propagated. Unfortunately it means that if two variables are unified, the attribute which would normally be thrown away must instead be kept, and kept in sync with the retained attribute, in case one or more constraints still have references to that attribute. Unifications do occur in "normal" programs, and the synchronisation is expensive. Plus, in a number of cases we want the actual variable instead or as well, which effectively negates the benefit. Hence we think constraints should keep references to the variables themselves rather than to the attributes.
In order to evaluate the overhead of dereferencing the variables, it is proposed to construct an experiment which simulates the access cost of a pass over a constraint stored using variables (for various chain lengths) against the same constraint stored using attributes.  By evaluating this overhead, it should be possible to decide which arrangement should be better without having to have both schemes implemented in the full-blown constraint solver.
Coefficient representation
Currently, constraint coefficients are stored in their "native" form (after floats are converted to bounded reals). Since these are then converted to a pair of floating point bounds and an integrality flag every time they are used, it may be worth storing them in the converted form instead (this may take more space, but should be faster).
It should be fairly straightforward to have both schemes implemented, with a compile-time switch between them, for comparison purposes.
Domain representation
Integer domains with elements excluded are currently represented using a bitmap.  This is impractical for very large domains, and inefficient (in space and time) for very sparse domains.  One alternative is to use a list of intervals representation á là the FD solver.  It's probably worth trying to devise a standard "holey domain" interface which would be supported by all alternative implementations, to facilitate interchanging between them (either for experimentation purposes or permanent substitution of one by another).
Constraint reduction
For the integer solvers developed for my PhD, it was shown that simplifying constraints as variables became ground was worthwhile. For the current implementation of IC, this is probably not true, at least when simplifying to the small "specialised" constraints, since that involves killing the old constraint and setting up the new one (which then gets propagated even though the old constraint has already done the propagation). In the new implementation, I plan to have (linear) constraints continue to look the same at the ECLiPSe level, even when simplification and specialisation is going on at the C level. Thus there will be no killing of constraints (unless they're entailed) and no setting up of new constraints just because the constraint has been simplified. With the reduction in overheads of the simplification, it should be the case that the simplification is worthwhile; that said, it should be straightforward to allow the simplification to be turned off at compile-time, for comparison purposes.
Waking frequency and suspension mechanisms
With constraints implemented as demons, equations always wake themselves, even if there is no need (though there may be need in some cases). It is possible to avoid this if IC manages its own waking lists, though "normal" ECLiPSe waking lists would also be required for use by the user. Note that such self-managed lists, if done in the right way, could be used to support substitutions (of one variable by a multiple of another, or "eager" ground substitutions) in future.
( What about reified (& other?) constraints which may not need to suspend on as many things as they used to once more information (e.g. the value of the boolean) is known? )
It appears that it may be possible to do variable by variable substitutions in a lazy fashion, in the same way that ground substitutions are done now (during propagation variables are checked to see if they're ground and if so, are substituted out). This would mean no special infrastructure would be needed for substitutions, but would incur the overhead of performing these checks on each variable accessed during propagation. It also may not be compatible with techniques for efficiently propagating long linear constraints (this would need further investigation).
With a modest amount of work it should be possible to have both standard ECLiPSe and self-managed waking lists implemented, with a compile-time switch between them, for comparison purposes.
( "Delayed suspension set-up" technique? )
Compile-time transforms
It is not clear how much benefit these actually bring. If the constraint was compile-time transformable and all the things that looked like variables at compile time are still variables at run time (i.e. they have not become ground) then the benefit is clear.  However, in order to limit the amount of code duplication, the compile-time transformations as they stand actually introduce a reasonable amount of overhead if the goal has to be processed at run time.  This is because the goal is first transformed (sharing code with the compile-time transformations), and then the transformed version call/1ed.
Unfortunately, many constraints are (re-)transformed at run time:
Note that even with the above, it is possible for a compile-transformed version of a constraint to be less efficient and less effective than that same constraint processed at run time, if a linear constraint contains two variables which look different at compile time but end up being the same variable at run time.
We propose changing the focus to making sure that run-time processing of a constraint is streamlined and efficient, with compile-time transformation machinery assessed for its impact on:
It would also be nice to get rid of the need for eval/1; this is an artifact of the way compile-time transformations are currently handled.  Apart from simply abolishing compile-time transformations, eval/1 could be avoided by making the constraints the transformations produce able to cope with a run-time expression in any place they currently expect a variable.
Note that the efficiency of constraint set-up is of most importance during search, and thus it is important to make sure that constraints which are likely to be imposed during search (typically simple constraints with one or two variables and a run-time constant) are set up efficiently.
Priorities
Floating point interval arithmetic
When propagating a constraint, all computation is done using interval arithmetic performed on floating point bounds, even when the constraint is an integer constraint.  Compared to a pure integer solver, this is potentially slower in two ways:
Note that neither of these overheads is simply wasted:
I do not expect to change either of these things in the rewrite, mostly due to the extra complexity they entail (having integer-based versions of the propagation code with overflow trapping, or threading flags about which bounds are of interest through all the places in the code which need to know this information).  Still, it may be worthwhile evaluating how much (best case) potential speed benefit there might be in using integer arithmetic, by creating quick-and-dirty pure integer versions of some of the linear constraints (ignoring overflow) and comparing the results (Andy Cheadle has already done some work along these lines).
Integer bounds (global constraints) & integer specialisation
"Strict" flag
In ECLiPSe 5.5, =< and < constraints were distinguished through the use of a "strict" flag which was passed to each relevant constraint. This flag (along with the reified boolean) resulted in a lot of if-then-else code (the boolean featured because the negation of =<, which is not strict, is >, which is strict; similarly with < and >=). This was a problem for two reasons:
Most of these problems can be avoided, however, by observing that X < Y is just the logical negation of Y =< X.  Thus, by "negating" the reification boolean associated with a constraint, any strict inequality can be transformed into a non-strict inequality.  In a similar fashion, any =\= constraint may be transformed into an =:= constraint.
This problem is addressed by the reification boolean toggle, with the constraint transformations and canonical forms covered in the discussion of the equation/inequation flag.
Constraint management
We believe there is currently too much "management" of constraints at the ECLiPSe level: manipulating them, transforming them from one kind to another (see constraint reduction), processing results returned from C-level propagators, etc.  Such management should probably be kept to a minimum, and as much as possible done in C (at least once the constraints are set up).

Design

A constraint goes through several layers / phases during its lifetime.  These are:
Preprocessing
Substitution of floats by bounded reals, constant folding, calling user-defined functions, etc.
Transformation
Breaking the constraint up into primitive constraints (linear constraints and nonlinear equations), putting them into normal form, etc.
Set-up
Creating suspensions, adding them to suspension lists, setting up any required data structures, etc.  This may be before initial consistency is achieved (using the propagation phase for this) or after (saving the cost of this set-up in the cases where attempting to achieve initial consistency would detect failure or entailment).
Propagation
Waking up on relevant changes and maintaining consistency, etc.
Entailment
Cleaning up any remaining suspensions, etc. if the propagation phase detects entailment.
Note that the above phases need not necessarily be distinct or in order either in the code or during execution.  For instance, the preprocessing may be done as part of the constraint transformation, the preprocessing and transformation phases may be split into compile-time and run-time components, and constraint set-up may be done after the first propagation pass.

That said, the preprocessing and transformation phases ought to be largely independent of the remaining phases.  As a result, we choose to defer the design of the preprocessing and transformation phases at this time, and concentrate for now on constraint set-up, propagation and entailment.  We also concentrate on linear constraints, since, as noted earlier, they are the most important; they may also be considered independent of the non-linear equation constraints.

Internal representation of linear constraints

Linear constraints are (conceptually) stored as LinExpr op Constant : Bool, where LinExpr is a list of Coefficient * Variable pairs, op is a relational operator, Constant and the Coefficients are ground numbers, Bool reflects the reification status of the constraint, and the Variables are (non-ground) variables.  They are actually stored in a structure with the following fields:
Flags
Equation/inequation
= or =<
Integrality?
Shouldn't be necessary once constraint set up, but might be worth remembering for printing?
Integrality propagation
Whether the constraint could potentially propagate integrality to one of its variables.
Reification boolean toggle
The reification boolean var/value is the opposite of what is intended.
Reification boolean variable
Reified boolean variable for the constraint.
RHS constant
The RHS constant of the constraint, either in "natural" form, or as integrality flag + bounds.
Term count
Count of the number of linear terms in the constraint.  Only needed if the LHS term stored as a vector, but might be useful for distinguishing 1, 2, and 3+ variable constraints.
LHS term vector/list
A vector or list of the linear terms on the LHS of the constraint.  Each term comprises:
Coefficient
Either in "natural" form, or as integrality flag + bounds.
Variable
Either as itself, or as its attribute.
Note that the flags, the RHS constant and the term count just contain ground "numerical" data, and so could be placed in a buffer structure.  This is likely to save at least a little memory; how much depends on other choices.  It can also help with trailing: the RHS constant and the term count are likely to change together, and they can either be updated in place or copied to a new buffer depending on whether there's been a new choice point since the last change.  Note that the integrality propagation flag can also change (at most once) and need not (though it usually will) coincide with a ground term elimination; if it needs to be trailed (rather than in-place update) that could either be done separately or a new buffer created (on the assumption that some other change is likely before the next choice point which would have required the new buffer anyway, and the fact that if not, the "damage" is limited since it can only happen once during a forward execution).

Equation/inequation flag

This flag is part of the constraint structure.

This flag addresses the "strict" flag performance issue, in conjunction with the reification boolean toggle.

This flag is fixed once the constraint is set up.

Ostensibly, this flag distinguishes between the different constraints

In practice,
 
LHSLinExpr =\= RHSConstant
    is the logical negation of  LHSLinExpr =:= RHSConstant
LHSLinExpr >= RHSConstant is just -LHSLinExpr =< -RHSConstant
LHSLinExpr > RHSConstant is the logical negation of LHSLinExpr =< RHSConstant
LHSLinExpr < RHSConstant is the logical negation of -LHSLinExpr =< -RHSConstant
and so (assuming the presence of the reified boolean toggle) we just need to be able to distinguish between =:= and =<.

Note that ECLiPSe 5.5 does not have just these two alternatives; it also has <, through the use of a "strict" flag, which complicates the inequality code considerably.

Integrality propagation flag

This flag is part of the constraint structure.

This flag may be set (i.e. to 1) when the constraint is set up, but after that it is only ever cleared (except for backtracking over such a clearing).

A general discussion of this flag and its purpose can be found in the section on integrality and propagation.

When the constraint is first set up, this flag is set only if:

It need not be set if: otherwise it should be set if permitted.

During execution, the flag is cleared if:

It may also (but need not) be cleared if: otherwise the flag should not be cleared.  It is expected that these checks and clearings may be done in the course of normal constraint propagation.

If the flag is set and there is exactly one remaining non-integer variable, that variable should be constrained to be integral if and only if:

Reification boolean toggle

This flag is part of the constraint structure.

This flag addresses the "strict" flag performance issue, in conjunction with the equation/inequation flag.

This flag is fixed once the constraint is set up.

For any linear constraint, the user has the option of reifying the constraint by providing a variable which reflects the status of the constraint (a value 1 corresponding to entailment or enforcing of the constraint, 0 corresponding to disentailment or enforcing the negation of the constraint). Assuming that we wish to have one form of any constraint regardless of whether this variable is set to 0, 1 or remains undefined, this means there must be code to propagate both the constraint and its negation using the same constraint format. This suggests there should be just one form used for both a constraint and its negation, so that the same code can be used to propagate, say,

since they are exactly the same constraint.  If the value of the reification boolean is provided at the time the constraint is set up then using just one form of the constraint is easy to support: if one is given the "negated" form, one simply substitutes the "normal" form and toggles the boolean. If, on the other hand, the reification boolean is still a variable, one could achieve the same effect by introducing a new boolean variable and constraining it to be the negation of the one provided, but this introduces an undesirable amount of overhead.

The alternative proposed here is to simply introduce a flag indicating whether the reification boolean should be "negated" whenever it is used or modified. This allows us to avoid introducing an extra boolean variable and constraint, and incurs minimal overhead: any value read from or written to the variable simply needs to be XORed with the flag. It also means we only need two types of linear constraint (see the section on the equation/inequation flag for more details).

Note that it would be nice for this toggle to occupy the "1" bit of the flags word to facilitate its easy extraction for XORing.

Reification boolean variable

This boolean variable is part of the constraint structure.

This variable may become instantiated after the constraint is set up.

This boolean is used to reflect or enforce the reification status of the constraint.  It is stored as a reference to the variable (rather than a reference to the variable's attribute) since once the constraint is set up, the attribute(s) of the variable are irrelevant; there are only three states of interest: the value 0, the value 1 or still a variable.

Note that the interpretation of this boolean is modified by the reification boolean toggle.

(Do a table?)

Numeric constant

Constants appear in several places in the constraint structure.

Their representation addresses the coefficient representation issue.

The value of these constants may or may not change, depending on the nature of the constant and whether constraint reduction is being performed or not.

Possible representations to be considered:

  1. Native ECLiPSe format.
  2. Integrality flag plus floating point bounds.
  3. Integrality flag plus floating point bounds, except treat specially the case where the bounds are equal so that only one floating point number needs to be stored.
We intend to start by comparing options 1 and 2, with other alternatives being considered later.

It turns out that the integrality of each individual constant in a linear constraint need not be stored; a single integer propagation flag is sufficient.  See the section on integrality and propagation for details.

RHS constant

This number is part of the constraint structure.

This number is an instance of a numeric constant, affected by the coefficient representation issue.

This number may be modified after constraint set-up if constraint reduction is being performed.

( Discuss representation? )

Term count

This integer is part of the constraint structure.

This integer is only needed if the LHS terms are being stored in vector form rather than list form and constraint reduction is being performed, but may also be useful for recognising the 1- and 2-variable special cases when using the list form.

This integer may be modified after constraint set-up if constraint reduction is being performed.

This integer should be stored adjacent to the RHS constant so that they may be trailed together (since they will usually both change at the same time).

This integer records the number of current entries in the LHS term vector (or list).

LHS terms

This list or vector is part of the constraint structure.

This list or vector may be modified after constraint set-up if constraint reduction is being performed.

This list or vector records the variables appearing in the linear constraint along with their coefficients.

The variables stored in this list or vector may become ground after constraint set-up.  If constraint reduction is being performed then any such ground variables will be removed from the representation.

We consider two basic forms for representing these linear terms:

List representation

This is the simpler of the two forms, and is what was in use before the rewrite.  The linear terms are stored in a list with one entry for each term, recording its variable and coefficient.  To save a little space, rather than using a normal list, one can simply add an extra field to the term structure giving the next term in the "list".

( Diagrams? )

The exact form of the structure depends on choices made in representing the coefficient (see the section on numeric constants).  One option would be to have the term structure contain three fields: the coefficient, the variable, and a pointer to the next term.  With this arrangement, looking at the tag in the coefficient field, one can distinguish between a structure or buffer containing a "normalised" coefficient, or any of the numeric types.  This would allow us to efficiently support multiple alternative coefficient representations by simply changing the code which creates these terms; any code for accessing or modifying existing terms would not need to change.

If constraint reduction is being performed, removing a term from this representation is simply a matter of setting the term's predecessor's "next" pointer to the same value as that of the term to be removed, trailing the change.  Note that this representation is stable with respect to such changes; that is, the remaining terms are in the same relative order as they were before the removal.

Vector representation

Management of this representation is a little more complicated than the list representation, but it should be more efficient in terms of both speed and memory consumption.  Conceptually, it consists of a vector with an entry for each term, recording its variable and coefficient.  In practice, however, the variables should go into a separate vector in order to make the most efficient use of memory.  This is because while the coefficient data can be safely packed into a buffer structure without the need for any ECLiPSe tags on the individual entries, this is not possible for variables since the garbage collector needs to know the location of all variables.  To store the variables, there are two obvious choices: For representing the coefficient vector in a buffer, there are a number of options available, depending on the choices made in representing the coefficient (see the section on numeric constants).  One interesting possibility is to have separate vectors for the upper and lower bounds of the coefficients; that way, if they are the same (e.g. for any integer constraint - as long as the coefficients are exactly representable as doubles) they can just both point to the same vector, saving space.

Where the vector representation gets interesting is when constraint reduction is being performed.  In such cases, when a variable becomes ground we wish to eliminate it from the vector.  Since we cannot just delete an entry from the middle, the obvious thing to do is copy the last entry in the vector to fill the hole.  (Note that this means that this representation is not stable: the relative order of terms can change.  It also means we cannot have any external references to terms in the constraint that depend on their positions.)  Since the terms no longer fill the vector we need a term count in order to keep track of where the currently valid terms end.  (This is necessarily distinct from the field in the vector's structure which records the size of that structure (and hence, indirectly, the initial number of terms in the constraint) for the garbage collector.)

In order to restore the constraint on backtracking, we need to have kept information about the removed term.  Rather than store this in the trail, we can store it in the newly unused location at the end of the vector.  That is, rather than simply overwriting the eliminated term with the last one, we can exchange their positions in the vector instead; on backtracking we just exchange them back.  (Note that if the upper and lower bounds of the coefficients are in "separate" vectors which may be shared if identical, we need to avoid swapping the same data twice in the shared case.  Note also that we need to swap the "raw" variable references, without any dereferencing, so that the references remain safe on backtracking.)  Better yet, on backtracking we don't need to swap the terms back, we can just leave them where they are: if we were free to move the last term to fill the hole, there's no reason we can't just arbitrarily rearrange them as we like.  This means we only need to trail the old values of the RHS constant and the term count; moreover, we only need to do this once for a sequence of reductions performed when propagating the constraint (e.g. if a number of variables have become ground since the last time the constraint was propagated), which probably means there's little benefit to be gained from doing timestamping on this data.  (Though if we put them in a separate (buffer) struct from the main constraint struct and trail the struct rather than the individual fields, we get timestamping anyway.)

Note that if multiple variables might have become ground, the following algorithm moves all the non-ground terms to the front of the vector with the minimal number of swaps:

It ought to be possible to incorporate the above into most propagation algorithms (at least, the ones which proceed through the constraint in a linear fashion but don't really care about the order); it simply corresponds to processing the terms in the order they are after the swaps rather than the order they were in before them.

Internal representation of variable attributes

This is expected to be more-or-less the same as the current version of IC, though we may consider moving some of the fields into a buffer structure or something, and we will be considering alternative representations of (holey) integer domains.  Where practical, access to a variable's data should be mediated through a set of access routines, in order to facilitate trying different alternatives.
 
Flags
Integral
Lower bound
Upper bound
Finite domain
Representation of holey integer domain, if required.
ID?
Could be useful for giving stable variable sort order (which is useful for detecting multiple occurrences of a variable).
[waking lists]

Linear constraint set-up

I envisage having a single predicate (implemented in C as far as practical) for setting up any linear constraint.  The input constraint is conceptually of the form LinExpr op 0 : Bool, where LinExpr is a list of Coefficient * Variable pairs, op is a relational operator, the Coefficients are ground numbers, Bool reflects the reification status of the constraint, and the Variables are (non-ground or ground) "variables".  The representation of the constraint would be via the following fields:
Operator
=:= or =< (or perhaps any of <, >=, >, =\=?).
Integrality flag
Whether to impose integrality on all the variables and coefficients.
Reification boolean
Whether the constraint is to be imposed, negated, or unknown.
Reification boolean toggle
Whether the reification boolean is the opposite of what is intended.  (Not needed if the full set of operators allowed.)
Linear expression
A list of linear terms of the form A*X where A is ground but X may be a variable.
The integrality flag and reification boolean toggle could be incorporated into the operator field by allowing the full set of relational operators (including all the # operators), but this does not seem useful since (prior to calling this set-up predicate) the operator will already have been examined, at which point it might as well be decomposed fully.  (See the equation/inequation flag section for a description of how the operator and boolean toggle fields relate to each other.)

The set-up predicate would be responsible for making sure that all variables are constrained to be IC variables, and, if integrality is required, imposing integrality on all the variables and checking the integrality of all the constants.  It would also construct the internal representation of the constraint, enforce initial consistency and set up any required suspensions, etc. (though the initial consistency and suspension creation may be delegated to the propagation code if appropriate).

Linear constraint propagation

As discussed above, we want to avoid having a constraint change its form (or really, its suspension) over its lifetime.  Hence the same predicate will be called with the same format of data to restore consistency for all kinds of linear constraints.  However, it is expected that, for efficiency, it will be worth distinguishing between:
one variable constraints
two variable constraints
These would use the obvious simple algorithms, and would probably also specialise based on the operator and/or the state of the reification boolean.
longer constraints
These would use the "two-pass" propagation algorithm or one of its variants (except for =\= constraints, which warrant a different algorithm), and may use general code regardless of the operator and reification boolean state.
It is expected that always storing the constraints in general form and performing some switching to choose the correct specialised algorithm will result in little overhead, and will allow constraint reductions to be performed without the high cost of creating and destroying suspensions that is currently incurred.

( Talk about how we always use floating point interval math even when it's not needed; discuss possible alternatives? )

Integrality and propagation

In earlier versions of IC, it was important to know whether each intermediate result was integral or not, in order to improve accuracy: bounds which should be integral but did not have an integral value due to floating point computations could be rounded in to the next integral value.  With the use of processor rounding modes, however, this is no longer necessary: if a bound should have an integral value, it will have one.  This means the only thing integrality of coefficients, intermediate results, etc. is needed for now is deducing when a non-integer variable should be constrained to be integral.  For example, consider the constraint
    X + Y =:= 3
If Y is an integer variable, then this implies that X should be an integer variable too, since giving Y any value from its domain will result in an integer value for X.  In this case, the integrality of X can be deduced just from looking at the constraint and the types of the variables appearing in it.  In other cases integrality may depend on the values taken by the variables:
    2 * X + Y =:= 3
In this case, X is integral only if Y ends up being fixed to an odd number.

Some observations about integrality:

( Talk about variables getting values from bounds (all remaining variables grounded simultaneously) vs values (value of last variable computed based on (externally-supplied) values of other variables)? )

This means we don't need to store the integrality of the coefficients or the RHS constant; instead we can have one flag for the whole constraint, indicating the potential for integer propagation, which is cleared as soon as we know that no such propagation can occur.  Then if the flag is set and when propagating the constraint we discover that there is one remaining non-integer variable and it has a unit coefficient, or if we're about to ground the only-remaining non-integral variable to an integral value, we impose integrality.

Flag for "potential int propagation".  Set initially iff (non-integer) eqn, all coefs integral, at least one var not integral (if only one non-int var and it has unit coef, simply make the var int and clear the flag).  If any var grounded to non-int value, clear flag.  If notice all vars integral, clear flag?  If notice only one var non-integral and has unit coef, make integral.  If grounding last var and flag set and value integral, make var integer.

It would be nice to have integrality propagation separate from the bounds propagation, so that it doesn't have to be incorporated into every bounds propagation algorithm we implement and so that it need not impose any overhead if it's not needed (many constraints in practice won't need it).  But a separate propagator makes it hard to ensure a variable is made integral (if appropriate) before being assigned a value by the main propagator.

Linear constraint entailment

( Talk about detecting entailment, killing suspensions, leaving delayed goals if the constraint is ground but not properly entailed, etc. )

Other notes / ideas / etc.

Have IC code call i_add, i_mul, etc. directly rather than going through ec_ria_binop.

Try to avoid setting up the constraint if first propagation pass is all that is ever needed.

Experiments

This section describes the experiments performed and evaluates the results.

Attributes vs. variables

Aim
Assess the overhead of dereferencing variables to access their attributes as part of the constraint propagation process.
Assumptions
We will only look at linear constraints; the overhead is expected to be similar for other kinds of constraints.
We will assume that the linear constraint is stored as a list of Coefficient * Variable terms.  Other ways of storing or representing the linear terms may have somewhat different overheads, but for the most part we're interested in the amount of extra overhead introduced by the variable representation so this should not be a problem.
We will assume that the number of linear terms in the constraint does not affect the average dereferencing cost per variable, so that we can use suitably long constraints in order to help obtain a reasonably measurable CPU time.
We will assume that the arrangement of the variables and terms, etc. in memory does not affect the average dereferencing cost per variable, so that we need not do anything fancy when setting up the constraints.  (Note that this is probably the most dubious assumption; it may be worth doing some kind of randomised arrangement.)
We will not look at linear constraints containing ground "variables", though the overhead of detecting/accessing these for each representation may also be worth looking at (though the attribute version is likely slower than the variable version).
Method
Construct a list of variables such that the reference to each variable in the list is the head of a variable chain of desired length (e.g. 5, 10, etc.).
Give each variable an IC attribute.
Construct a list of Coefficient * Attribute terms and a list of Coefficient * Variable terms based on the list (using, for example, 1.0 as the coefficients).
Check that, for the Coefficient * Variable list, the variables do actually need dereferencing the appropriate number of times.
For the Coefficient * Attribute list, perform a number of passes over the list, extracting one of the fields of the attribute (e.g. the lower bound), and record the total time taken.
Repeat for the Coefficient * Variable list.
Adjust the length of the "constraint" and the number of passes over the list in order to get a run time which is large enough to be measureable.
Repeat the experiment for a number of different variable chain lengths and a number of different architectures.
Results
dog, using 5.6 #6
Attribute Variable (1) Variable (5) Variable (10)
access lwb, 1000/1000 0.58 1.26 1.58 2.14
nodbgcomp, access lwb, 1000/1000 0.47 0.88 1.44 2.45
Note the last column!  Turning off debugging slows it down!  This symptom also reproducible on alpha_linux: turning off variable names results in dereferencing the reference chains taking about twice as long!
Without more information about typical variable chain lengths and how often IC variables get unified, there's no clear result here.  It shouldn't be too hard to collect statistics on this kind of thing, and we probably should do this at some point, but since it should be relatively easy to have both alternatives implemented, the conclusion is to just implement them both and try them out on real programs to see how they do.