Consider the following simple interaction with SICStus Prolog:
$ sicstus -f SICStus 4.8.0 (x86_64-linux-glibc2.28): Sun Dec 4 13:17:41 UTC 2022 [...] | ?- use_module(library(between)), use_module(library(lists)). [...] | ?- compile(user). % compiling user... | f(X) :- integer(X). | | fs([]). | fs([F|Fs]) :- f(F), fs(Fs). | | maplist_f(Xs) :- maplist(f,Xs). | % compiled user in module user, 64 msec 698032 bytes yes
I was expecting that fs/1
and maplist_f/1
have pretty much the same performance—thanks to the use of "logical loops." What I got with a simple test, however, is this:
| ?- statistics(runtime,_), (numlist(1000,Xs),repeat(100000),fs(Xs),false;true), statistics(runtime,[_,RT]). RT = 284 ? yes | ?- statistics(runtime,_), (numlist(1000,Xs),repeat(100000),maplist_f(Xs),false;true), statistics(runtime,[_,RT]). RT = 2758 ? yes
The variant using maplist/2
is ~10X slower: what's going on?!
The SICStus Prolog profiler puts the blame on meta-calls—but why are they even used here?
As false mentioned in a comment, the logical loops do not do anything clever with the meta argument. Instead the effect of maplist_f(Xs)
is pretty much equivalent to:
:- meta_predicate(mfs(+, 1)).
mfs([], _G1).
mfs([F|Fs], G1) :- call(G1, F), mfs(Fs, G1).
mfs_f(Fs) :- mfs(Fs, f).
And, unsurprisingly, passing an extra meta argument (f
) and calling it with call/2
, has a significant cost compared to the direct call done by fs/1
.
In fact, what may be more surprising, calling maplist_f(Xs)
(which uses logical loops internally) is slightly slower than calling mfs_f(Fs)
(which uses an ordinary predicate, with first argument indexing).