repeat
repeat

Reputation: 18726

maplist/2 vs extra recursion predicate with SICStus Prolog

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?

Upvotes: 1

Views: 123

Answers (2)

jschimpf
jschimpf

Reputation: 5034

To take advantage of the compile-time expansion of logical loops, you have to use them directly, not indirectly via a maplist call. In your example, your should therefore compare fs/1 against a loop_f/1 like

loop_f(Xs) :- ( foreach(X,Xs) do f(X) ).

and these should have pretty much the same performance, because the do/2-construct is compile-time-expanded into metacall-free code.

By the way, it is absolutely possible to do the same with the maplist construct, as implemented in library(apply_macros) [2,3], which was effectively a precursor to logical loops.

Upvotes: 2

Per Mildner
Per Mildner

Reputation: 10487

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).

Upvotes: 3

Related Questions