prolog icon indicating copy to clipboard operation
prolog copied to clipboard

Speeding up program execution

Open riccardopinosio opened this issue 2 years ago • 4 comments

Hi @ichiban

I was wondering if there's perhaps a way to add facts to the prolog interpreter without going through the p.Exec() route, which parses a string, but rather by calling methods on p directly. The reason I ask is that I'm trying to speed up the process of adding and removing facts from the same interpreter p, as I have an application where I need to use the same underlying p to query against many differing sets of facts. It was suggested in another issue to look into association lists/AVL trees for this but I first wanted to see if there's speeding up options within the current library.

For instance, would it be possible to add a fact P(a). by directly calling the p.Assertz function rather than going through .Exec? Or is there even a lower level entry point that would allow to skip the string parsing and hopefully gain some performance?

riccardopinosio avatar Mar 15 '22 21:03 riccardopinosio

Is string parsing expected to be such a tremendous overhead that it is worth thinking about? If it is, then it seems best to first improve the parser, so that it becomes as fast as needed to quickly handle these facts.

triska avatar Mar 15 '22 21:03 triska

Good question. What I see in the testing with the data I have is that the repeated execution with p.Exec of sets of assertz and retract statements for the different sets of facts seems to be the speed bottleneck, rather than the actual querying. I will try to do some more profiling to see if it's the parser or if there's gains to be had somewhere else in the code.

riccardopinosio avatar Mar 16 '22 07:03 riccardopinosio

Also I wonder if .Exec is thread safe. What happens if .Exec on the same program P is called from multiple goroutines? that could be a way to parallelize the writing of the facts to the db.

riccardopinosio avatar Mar 16 '22 08:03 riccardopinosio

@riccardopinosio Hi! You can actually skip p.Exec() like this:

package main

import (
	"context"
	"fmt"

	"github.com/ichiban/prolog"
	"github.com/ichiban/prolog/engine"
)

func main() {
	p := prolog.New(nil, nil)

	t := &engine.Compound{Functor: "foo", Args: []engine.Term{engine.Atom("a")}}

	if _, err := p.Assertz(t, engine.Success, nil).Force(context.Background()); err != nil {
		panic(err)
	}

	sols, err := p.Query(`foo(X).`)
	if err != nil {
		panic(err)
	}

	for sols.Next() {
		var s struct {
			X string
		}
		if err := sols.Scan(&s); err != nil {
			panic(err)
		}
		fmt.Printf("X = %s\n", s.X)
	}
}

Note that the interface of github.com/ichiban/prolog/engine is unstable and this trick might not be available in the future releases.

The DB is just a map and not goroutine safe.

https://github.com/ichiban/prolog/blob/6d174c70bca7f45597f494dae19b9219764f8e6e/engine/vm.go#L51

ichiban avatar Mar 16 '22 13:03 ichiban

As of v0.15.0, the hack above can be done like this:

package main

import (
	"context"
	"fmt"

	"github.com/ichiban/prolog"
	"github.com/ichiban/prolog/engine"
)

func main() {
	p := prolog.New(nil, nil)

	t := engine.NewAtom("foo").Apply(engine.NewAtom("a"))

	if _, err := engine.Assertz(&p.VM, t, engine.Success, nil).Force(context.Background()); err != nil {
		panic(err)
	}

	sols, err := p.Query(`foo(X).`)
	if err != nil {
		panic(err)
	}

	for sols.Next() {
		var s struct {
			X string
		}
		if err := sols.Scan(&s); err != nil {
			panic(err)
		}
		fmt.Printf("X = %s\n", s.X)
	}
}

ichiban avatar Dec 17 '22 02:12 ichiban

We've done a couple of performance tunings since you submitted this issue. I bet it's way faster in many use cases. I'll close this issue for now. Feel free to reopen it or submit a new one!

ichiban avatar Dec 17 '22 02:12 ichiban