1

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?

false
  • 10,264
  • 13
  • 101
  • 209
repeat
  • 18,496
  • 4
  • 54
  • 166
  • 1
    Note that "logical" loops are used to (incorrectly, that is incompletely) implement `maplist/2`. They do not expand the metaargument at all. – false Feb 27 '23 at 19:30
  • 1
    With a pure goal instead, you would also see the deficiencies of `maplist/2`. Like its incorrect failure of `?- maplist(=(X),L), L = [_|_]. false, unexpected.` – false Feb 28 '23 at 07:40

2 Answers2

3

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

Per Mildner
  • 10,469
  • 23
  • 30
  • Good to know. The profiler told me me something similar:) So if this uses `call`, using "logical loops" for performance reasons makes no sense, right? – repeat Feb 27 '23 at 20:30
  • I was optimizing `library(clpz)` which does a lot of this `maplist/N` stuff. This was one thing that could be done better. – repeat Feb 27 '23 at 20:33
  • 3
    Logical loops should never be used for speed, only for compactness, if that. Ordinary predicates can utilize first-argument indexing for better performance, and logical loops can easily lead to surprising results for partial lists and the like. – Per Mildner Feb 27 '23 at 20:52
  • @PerMildner I can't quite agree with your categorical statement. Loops are not only more compact (by hiding auxiliary predicates, arguments and variables), they are easier to modify, and more explicit in revealing the intention of iteration and accumulators. The speed is essentially the same as the equivalent recursion (at least in Prolog systems like ECLiPSe with flexible argument indexing). As the implementation includes a cut, results can be surprising for applications where the number of iterations is not deterministic and finite. – jschimpf Mar 02 '23 at 13:26
2

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.

jschimpf
  • 4,904
  • 11
  • 24