Reputation: 2758
While playing with Logtalk, is seems my program was longer to execute with Logtalk object versus plain Prolog. I did a benchmark comparing the execution of the simple predicate in plain Prolog with the logtalk object encapsulation equivalent below :
%%
% plain prolog predicate
plain_prolog_simple :-
fail.
%%
% object encapsulation
:- object(logtalk_obj).
:- public([simple/0]).
simple :-
fail.
:- end_object.
Here’s what I get :
?- benchmark(plain_prolog_simple).
Number of repetitions: 500000
Total time calls: 0.33799099922180176 seconds
Average time per call: 6.759819984436035e-7 seconds
Number of calls per second: 1479329.3346604244
true.
?- benchmark(logtalk_obj::simple).
Number of repetitions: 500000
Total time calls: 2.950408935546875 seconds
Average time per call: 5.90081787109375e-6 seconds
Number of calls per second: 169468.0333888435
true.
We can see logtalk_obj::simple call
is slower than plain_prolog_simple
call.
I use SWI Prolog as backend, I tried to set some log talk flags, without success.
Edit : We can find benchmark code samples to https://github.com/koryonik/logtalk-experiments/tree/master/benchmarks
What's wrong ? Why this performance diff? How to optimize Logtalk method calls ?
Upvotes: 2
Views: 171
Reputation:
You can also make interpreters for object orientation which are quite fast. Jekejeke Prolog has a purely interpreted (::)/2 operator. There is not much overhead as of now. This is the test code:
Jekejeke Prolog 3, Runtime Library 1.3.0
(c) 1985-2018, XLOG Technologies GmbH, Switzerland
?- [user].
plain :- fail.
:- begin_module(obj).
simple(_) :- fail.
:- end_module.
And these are some actual results. There is not such a drastic difference between a plain call and an (::)/2 operator based call. Under the hood both predicate lookups are inline cached:
?- time((between(1,500000,_), plain, fail; true)).
% Up 76 ms, GC 0 ms, Thread Cpu 78 ms (Current 06/23/18 23:02:41)
Yes
?- time((between(1,500000,_), obj::simple, fail; true)).
% Up 142 ms, GC 11 ms, Thread Cpu 125 ms (Current 06/23/18 23:02:44)
Yes
We have still an overhead which might be removed in the future. It has to do that we still do a miniature rewrite for each (::)/2 call. But maybe this goes away, we are working on it.
Edit 23.06.2018: We have now a built-in between/3 and implemented already a few optimizations. The above figures show a preview of this new prototype which is not yet out.
Upvotes: 0
Reputation: 18663
A benchmarking trick that works with SWI-Prolog and YAP (possibly others) that provide a time/1
meta-predicate is to use this predicate with Logtalk's <</2
debugging control construct and the logtalk
built-in object. Using SWI-Prolog as the backend compiler:
?- set_logtalk_flag(optimize, on).
...
?- time(true). % ensure the library providing time/1 is loaded
...
?- {code}.
...
?- time(plain_prolog_simple).
% 2 inferences, 0.000 CPU in 0.000 seconds (59% CPU, 153846 Lips)
false.
?- logtalk<<(prolog_statistics:time(logtalk_obj::simple)).
% 2 inferences, 0.000 CPU in 0.000 seconds (47% CPU, 250000 Lips)
false.
A quick explanation, the <</2
control construct compiles its goal argument before calling it. As the optimize
flag is turned on and time/1
is a meta-predicate, its argument is fully compiled and static binding is used for the message sending. Hence the same number of inferences we get above. Thus, this trick allows you to do quick benchmarking at the top-level for Logtalk message-sending goals.
Using YAP is similar but simpler as time/1
is a built-in meta-predicate instead of a library meta-predicate as in SWI-Prolog.
Upvotes: 1
Reputation: 18663
In a nutshell, you're benchmarking the Logtalk compilation of the ::/2
goal at the top-level INTERPRETER. That's a classic benchmarking error. Goals at the top-level, being it plain Prolog goals, module explicitly-qualified predicate goals, or message sending goals are always going to be interpreted, i.e. compiled on the fly.
You get performance close to plain Prolog for message sending goals in compiled source files, which is the most common scenario. See the benchmarks
example in the Logtalk distribution for a benchmarking solution that avoids the above trap.
The performance gap (between plain Prolog and Logtalk goals) depend on the chosen backend Prolog compiler. The gap is negligible with mature Prolog VMs (e.g. SICStus Prolog or ECLiPSe) when static binding is possible. Some Prolog VMs (e.g. SWI-Prolog) lack some optimizations that can make the gap bigger, specially in tight loops, however.
P.S. Logtalk comes out-of-box with a settings configuration for development, not for performance. See in particular the documentation on the optimize
flag, which should be turned on for static binding optimizations.
UPDATE
Starting from the code in your repository, and assuming SWI-Prolog as backend compiler, try:
----- code.lgt -----
% plain prolog predicate
plain_prolog_simple :-
fail.
% object encapsulation
:- object(logtalk_obj).
:- public(simple/0).
simple :-
fail.
:- end_object.
--------------------
----- bench.lgt -----
% load the SWI-Prolog "statistics" library
:- use_module(library(statistics)).
:- object(bench).
:- public(bench/0).
bench :-
write('Plain Prolog goal:'), nl,
prolog_statistics:time({plain_prolog_simple}).
bench :-
write('Logtalk goal:'), nl,
prolog_statistics:time(logtalk_obj::simple).
bench.
:- end_object.
---------------------
Save both files and then startup Logtalk:
$ swilgt
...
?- set_logtalk_flag(optimize, on).
true.
?- {code, bench}.
% [ /Users/pmoura/Desktop/bench/code.lgt loaded ]
% (0 warnings)
% [ /Users/pmoura/Desktop/bench/bench.lgt loaded ]
% (0 warnings)
true.
?- bench::bench.
Plain Prolog goal:
% 2 inferences, 0.000 CPU in 0.000 seconds (69% CPU, 125000 Lips)
Logtalk goal:
% 2 inferences, 0.000 CPU in 0.000 seconds (70% CPU, 285714 Lips)
true.
The time/1
predicate is a meta-predicate. The Logtalk compiler uses the meta-predicate property to compile the time/1
argument. The {}/1
control construct is a Logtalk compiler bypass. It ensures that its argument is called as-is in the plain Prolog database.
Upvotes: 4