[comp.lang.prolog] theorem prover of R. Overbeek

bimbart@kulcs.uucp (Bart Demoen) (02/23/88)

I have been busy with the theorem prover of R. Overbeek, which was used
as a benchmarking program and a challenge to the prolog community. I want
to report here on my experiences with changing the program, introducing
destructive assignment and finding a reason why his prolog program is 20
times slower than his C program for this problem.

The unchanged version of the program took 77 sec in BIMprolog, on a SUN3/50
with the boys example (it is slow, I know, the main reason is that the bit
operations on integers are not yet in assembler, they are interpreted now;
and if you try to make time measurements, you will notice how much time the
program spends in get_next_literal and get_literals)

The first change I made to the program, was to replace the definition of
num_lits/3:

	num_lits(_p,_n,_num) :- c(_p,0,_m) , c(_n,_m,_num) .

	c(0,_i,_i) :- ! .
	c(_n,_i,_j) :- _k is _n / 2 ,
			_i1 is _i + _n - 2 * _k ,
			c(_k,_i1,_j) .

This reduced the runtime to 70 seconds !

By giving another prolog definition to get_next_literal and get_literals, I
gained another 22 seconds ! (I am a bit ashamed about this prolog definition,
so I will not distribute it over the net, but you can obtain it from me
privatly)

Some small changes involved:

	the test for tautology before the test for forward_subsumed
		(that's how it is in C !)

	form_resolvent/7 is symmetric in the first 2 argument pairs (at least
	that's the way it is used) so there is no need to have 2 definitions
	of form_resolvent/8; accordingly the definition of form_resolvent/7
	becomes

		form_resolvent(Par1Pos,Par1Neg,Par2Pos,Par2Neg,
          	     	ClashLit,ResPosWord,ResNegWord) :-
			Mask is \(1 << (ClashLit - 1)),
     			ResPosWord is ((Par1Pos \/ Par2Pos) /\ Mask ),
      			ResNegWord is ((Par2Neg \/ Par1Neg) /\ Mask).
	(it looks so in C !)

This gained only 1 second.

Then, I replaced all if-then-else constructions by their equivalent with
disjunction and cut: for instance:

	delete_from_database_clauses([Id|Rest],L,Db,NewDb) :-
        (   member(Id,L) ->
                delete_from_database_clauses(Rest,L,Db,NewDb)
        ;   /* otherwise -> */
                delete_from_database(Db,Id,Db1),
                delete_from_database_clauses(Rest,[Id|L],Db1,NewDb)
        ).

became:

	delete_from_database_clauses([Id|Rest],L,Db,NewDb) :-
        (   member(Id,L) , ! ,
                delete_from_database_clauses(Rest,L,Db,NewDb)
        ;   /* otherwise -> */
                delete_from_database(Db,Id,Db1),
                delete_from_database_clauses(Rest,[Id|L],Db1,NewDb)
        ).

Gain: 4 seconds.

The program took now about 44 seconds.

Then, I introduced destructive assignment into the program, as much as possible
without changing the algorithm. So, argrep/4 and delete/3 and
form_updated_index/4 were rewritten with destructive assignment and time
dropped from 44 to 22 seconds.
Actually, there was a small problem with destructive assignment, because at
one place in the program, the old database is used after the new one has been
constructed:

subsume_and_add(Db,ResPos,ResNeg,Parents,NewDb,DelList,NewEnd) :-
        ( (forward_subsumed(Db,ResPos,ResNeg) ; tautology(ResPos,ResNeg) ) ->
                NewDb = Db,
                NewEnd = DelList
        ;   /* otherwise -> */
                add_to_sos(Db,clause(Parents,ResPos,ResNeg),NewDb,NewId),
                write_added_mesg(NewId,clause(Parents,ResPos,ResNeg)),
                back_subsumption(Db,NewId,ResPos,ResNeg,DelList,NewEnd)
        ).                       ^^
                                 ||

In the call to back_subsumption, the first argument can be NewDb, but then in
the second definition of backsub/6, after the call to subsumes/4, an extra test

			Id =\= SubId

must be added. (the C code also shows this test)

For destructive assignment, I implemented (in the BIMprolog kernel of course) a
predicate

	replaca/3 	arg1 : a compound term
			arg2 : an integer between 1 and the arity of arg1
			arg3 : anything

		effect : in arg1, the arg2'th argument is replaced by arg3

and then changed the program accordingly; for instance: the new definiton of
argrep/4:

	argrep(N,Old,Value,Old) :- replaca(Old,N,Value) .


For a destructive delete/2, I had to do something similar.

It is clear that destructive assignment improved the speed, but still the prolog
program is slower by a factor at least 10 than the C version.
With destructive assignment, the program needed 2.5 times less heap.

Not only the lack of destructive assignment is the performance killer in this
program: let's look at a typical sequence in the prolog program and compare it
with the C version:

backsub(...)
        :-
        (   subsumes(SubsumerPos,SubsumerNeg,Pos,Neg) , Id =\= SubId , ! ,
                write_subsumed_mesg(Id,SubId),
                add_element(DelList,Id,EndList)
        ;
                EndList = DelList
        ),
        backsub(...).


    for (pt = lst->next_avl - 1; pt >= lst->first; pt--)
    {
        if ((formula_id != (*pt)->id) && Subsumes(cl,(*pt)))
        {
            printf("clause %d subsumes %d \n", formula_id,(*pt)->id);
            add_to_deleted_list((*pt)->id);
        }
    }

Most prologs recognise tailrecursion and can compile it to iteration, all right.
But, instead of C's if-then-else, prolog creates a choicepoint, makes the test
subsumes/4 (which is an out-of-line call while Subsumes in C is a macro) and
either calls a general procedure to fail, or continues and executes a cut: the
prolog overhead is at least a factor of 50 compared to C ! It is almost a
surprise that prolog is only 20 times slower !
Moreover, the datastructure used in the C and prolog program for sets, (a bit
string, i.e. an integer) is very much in favour of C: in a 'typed' prolog were
dereferencing would not be needed, arithmetic could approach the speed of
C arithmetic, but not in untyped prolog.
Since prolog compiler technology is still immature, the introduction of
destructive assignment improved the speed of the program with only a factor
of 2 (from 44 to 22 seconds) which is not much; but perhaps, in a couple of
years, when we will have implemented all beautiful ideas floating around right
now, the improvement will be from 24 to 2 seconds, making destructive assignment
well worth the effort !

ed298-ak@violet.berkeley.edu (Edouard Lagache) (02/28/88)

In article <1150@kulcs.UUCP> bimbart@kulcs.UUCP (Bart Demoen) writes:
>I have been busy with the theorem prover of R. Overbeek, which was used
	.
	.
	.
>For destructive assignment, I implemented (in the BIMprolog kernel of course) a
>predicate
>
>	replaca/3 	arg1 : a compound term
>			arg2 : an integer between 1 and the arity of arg1
>			arg3 : anything

	^^^^^^^^^


	Here is an item that has interested me almost since I started using
	PROLOG.  Why aren't destructive list predicates a part of PROLOG?
	I must say that I haven't missed them much in my programming but I
	have been avoiding lists with a passion.  When lists and speed are
	needed, destructive list predicates would seem to be desirable.

	So what you do say folks?  Shouldn't destructive manipulation be
	a part of PROLOG (or is it a part of most PROLOGs already?)

							Edouard Lagache

ok@quintus.UUCP (Richard A. O'Keefe) (02/29/88)

In article <7244@agate.BERKELEY.EDU>, ed298-ak@violet.berkeley.edu (Edouard Lagache) writes:
> In article <1150@kulcs.UUCP> bimbart@kulcs.UUCP (Bart Demoen) writes:
> >I have been busy with the theorem prover of R. Overbeek, which was used
> 	.
> >For destructive assignment, I implemented (in the BIMprolog kernel of
> >course) a predicate
> >	replaca/3
> 
> 	Here is an item that has interested me almost since I started using
> 	PROLOG.  Why aren't destructive list predicates a part of PROLOG?

Oh dear oh dear oh dear oh dear.  Have you ever felt that everything you
have done for the last N years has been futile?  Have you ever been
telling people about the milk and honey of the Promised Land, only to
have them turn around and say "What about leeks?  We had great leeks in
Egypt."

Let's clear up a misconception.  Demoen's rplaca/3 is NOT an operation
on lists as such, it's the equivalent of an array element update (see
'$SET' in some versions of C Prolog, setarg/[3,4] in some other Prologs).

But didn't you spot the punch-line in Bart Demoen's posting?  He did all
those things, including adding rplaca/3 , AND IT STILL DIDN'T HELP.  The
time went down from 77 seconds to 22 seconds (the last reduction was
from 44 seconds to 22 seconds).  However,

(a) the original version of the program took 26 seconds in Quintus Prolog
(b) Tim Lindholm was able to reduce that by a large factor (I haven't the
    figures to hand, but it was about a factor of 2, and did *not*
    involve the use of any sort of destructive operations).
(c) the C version took 1.18 seconds
(d) there is reason to believe that the C version could go twice as fast
(e) these times are times on one specific problem.  It's not enough
    to get close to C speed on this problem.  The method used has to
    SCALE at least as well as the C version scales.

The whole problem with Overbeek's Prolog program is that it faithfully
mirrors the structure of his C program.  The way to obtain a fast Prolog
program is to go back to the specification, and write a Prolog program,
instead of writing a C program in Prolog.

> 	So what you do say folks?  Shouldn't destructive manipulation be
> 	a part of PROLOG (or is it a part of most PROLOGs already?)

Look, if you want to use destructive operations, please use Lisp.
Lisp is GOOD at that sort of thing, though I would warn you that in
some Lisp systems rplacd is very bad for efficiency.  The whole point
of Prolog is that it is supposed to be based on logic.  Destructive
operations simply have no place in that model.  Which is not to say
that arrays with unit expected-time update do not!  There were several
papers about them, and one technique was posted to the Prolog Digest.
Is anyone out there using a Prolog with logical arrays?  What's it like?

Why are so many people chasing the "destructive operations" pennies
when there are "better data structure" POUNDS to be won?

steve@hubcap.UUCP ("Steve" Stevenson) (03/02/88)

in article <710@cresswell.quintus.UUCP>, ok@quintus.UUCP (Richard A. O'Keefe) says:
> 
> Look,... Which is not to say that arrays with unit expected-time
update do not!  There were several papers about them....

I would appreciate a reposting of the papers and / or techiques.  Of
quite a bit of interest to me is how to deal with something like
a vector Newton-Raphson or Jacobi method.

-- 
Steve (really "D. E.") Stevenson           steve@hubcap.clemson.edu
Department of Computer Science,            (803)656-5880.mabell
Clemson University, Clemson, SC 29634-1906

seif@sics.se (Seif Haridi) (03/10/88)

Regarding destructive assignment in Prolog, here is our experience at SICS
with this feature. SICStus Prolog (version 0.6 will be announced soon
for external use) has a backtrackable assignment called setarg/3:
	setarg(+ArgNo,+CompoundTerm,?NewArg)
which replaces destructively argument ArgNo in CompoundTerm with NewArg and
the assignment is undone on backtracking. There is also a lower level
nonbacktrackable assignment not available for the users.

We had this feature for a while to evaluate its need. My current view is
that setarg/3 is not needed as is but it has been used to implement in SICStus
Prolog other features that are important for efficiency.

1. setarg/3 has been used to implement a number of predicates for manipulating
logical arrays with constant access time for the latest array reference.

2. Also it has been used for implementing logical hash tables (with the
possibility of removing items from the hash table).

These features are needed in a project on natural language processing at SICS
where a grammar dictionary is created from reading an analysing a text in
natural language.  The features above can of course be used on other Prolog
conventional applications.

3. Another primitive that is now existing in SICStus 0.6 is an :
	if(+P,+Q,+R) which is: if P then Q else R
this is similar to (P -> Q ; R) construct except that it allows the
generation of all possible solutions for P. This construct was needed on
a work going on a complete theorem prover being built at SICS for
intuitionistic first order logic. This control primitive was implemented
efficiently first after using the lower primitive nonbacktrackable
destructive assignment.

Another area where we thought first that assignment is important is the
area of object oriented programming as in SmallTalk (mutable objects)
specially for the important class of discrete event simulation applications.
The way out was to do object oriented programming in SICStus in a way
that is similar to committed choice languages (CCLs). That is to introduce
a data-driven control element in the language above the normal goal driven
mechanism. This is done by introducing wait declarations in the language and
device a reasonably efficient suspension mechanism. This leads to a language
with the added expressive power of CCLs on sequential machines but it also
allows backtracking over streams.
A problem arose here is how to create multiple reference to a shared object.
This is done as in CCLs by creating a tree of binary merge operators to
the shared object. This can be done and isolated in a single predicate
called merge/2 for doing the so called multiway merge.
A problem here is that sending a message to a shared object would not take
a constant time. The message has to pass through a number of merge operators.
This is not acceptable in all known object oriented languages.

4. The multiway merge/2 is again implemented by  setarg/3 reduces the
message sending delay to constant time (see Shapiro).

5. Another object in the style CCLs that is useful in is an array object that
can be implemented using setarg/3.

The above two features had a considerable effect on the speed of a number
of discrete event simulation programs that we wrote.

In summary my current point of view is that the use of setarg/3 can be
isolated and hidden in a number of predicates that still can be implemented in
a Prolog system ( in case of object oriented feature you need a data driven
control element). The use of setarg is then to increase efficiency without
changing the semantics of your program

	Seif Haridi

alf@sics.se (Thomas Sj|land) (03/11/88)

The ending point of Seif's message "without changing the semantics of your
program" should be read "without changing the semantics of your algorithm"
to make sense, I guess.

I (and also Seif, implicitly) mentioned the possibility to "implement"
setarg/3 using var/1 and !/0. Perhaps it should be clarified that what
is meant is that you can represent a variable that you wish to modify
during the execution as a partially instantiated structure (an open
list containing all values assigned to the variable). This is not exactly
an "implementation" since you cannot unify two "variables" represented in
this way if they do not share the same instantiation history, but it is a
practical programming technique, that is appropriate in many situations,
if you do not wish to use setarg/3.
The efficiency problem mentioned by Seif should be obvious from this.
The semantic problems seem to be similar to those introduced by viewing 
i.e. [1.2.3|X]-X as the same d-list regardless of what X is bound to. 
In both cases you READ your program in a particular way, and JUSTIFY your 
statement about equality of "the semantics" by some more or less 
well-specified METHODOLOGY for implementing a given algorithm.