☰

Efficiency Issues in Prolog

Computational Logic


Efficiency

  • In general, efficiency ≡ savings:
    • Not only time
      (number of unifications, reduction steps, LIPS, etc.)
    • Also memory
  • General advice:
    • Use the best algorithms
    • Use the appropriate data structures
  • Each programming paradigm has its specific techniques, try not to adopt them blindly.

  • Note: Absolute times obviously depend on the system (computer, Prolog implementation, etc.), so the relevant measure is the speedup observed, which is what we show in the examples (measured in Ciao 1.26). Try them on your system with the run buttons!

Data structures

  • Do not make excessive use of lists:
    • In general, only when the number of elements is unknown
    • It is convenient to keep them ordered sometimes (e.g., set equality)
    • Otherwise, use structures (functors):
      • Less memory
      • Direct access to each argument (arg/3) (like arrays!)

Data structures (Contd.)

  • Use advanced data structures:
    • Sorted trees
    • Incomplete structures
    • Nested structures
    • …
  • D.H.D. Warren: "Prolog means easy pointers"

Let Unification Do the Work

  • Unification is very powerful. Use it!
  • Example: Swapping two elements of a structure:

    • Slow, difficult to understand, long version (exaggerated):

      :- module(_,_).
      
      swap_slow(S1, S2) :-
          functor(S1, f, 2),
          functor(S2, f, 2),
          arg(1, S1, X1),
          arg(2, S1, Y1),
          arg(1, S2, X2),
          arg(2, S2, Y2),
          X1 = Y2,
          X2 = Y1.
    • Fast, intuitive, shorter version:

      swap(f(X, Y), f(Y, X)).
      Try:

      ?- swap_slow(f(a,b),S).
      ?- swap(f(a,b),S).
    • The speedup, swapping a million times:

      swaps_slow(0).
      swaps_slow(N) :-
          N > 0,
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          swap_slow(f(a,b), _),
          N1 is N - 1,
          swaps_slow(N1).
      
      swaps(0).
      swaps(N) :-
          N > 0,
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          swap(f(a,b), _),
          N1 is N - 1,
          swaps(N1).
      
      swaps_loop(0).
      swaps_loop(N) :-
          N > 0,
          N1 is N - 1,
          swaps_loop(N1).
      ?- speedup_loop(swaps_loop(100000), swaps_slow(100000), swaps(100000)).
    • Auxiliary predicates, used in all the examples of this part: speedup(Slow, Fast) shows the time (in ms) of one run of each goal, and how many times faster Fast is. Each goal is repeated until it has run for at least 100 ms, and the time of the loop itself is subtracted, so that short goals can also be measured. speedup_loop(Loop, Slow, Fast) also subtracts the time of Loop (the empty loop around the calls being compared).

      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).

Let Unification Do the Work (Contd.)

  • Example: check that a list has exactly three elements.

    • Weak answer (always traverses the list and computes its length):

      :- module(_,_).
      
      three_elements_weak(L) :-
          length(L, N),
          N = 3.
    • Better:

      three_elements([_,_,_]).
      Try:

      ?- three_elements_weak([a,b,c]).
      ?- three_elements([a,b,c]).
      ?- three_elements_weak([a,b]).
      ?- three_elements([a,b]).
    • The times, for a list of N elements: that of the weak answer grows with N, while the better one is too fast to measure:

      test(N) :-
          numbers(N, L),
          speedup(three_elements_weak(L), three_elements(L)).
      ?- test(10000).
      ?- test(1000000).
      %! \begin{focus}
      numbers(0, []).
      numbers(N, [N|Ns]) :-
          N > 0,
          N1 is N - 1,
          numbers(N1, Ns).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).

Database

  • Avoid using it for simulating global variables

    Example (real executions):

    • Using the database to simulate a global variable:

      :- module(_,_).
      
      :- dynamic counting/1.
      
      bad_count(N) :-
          assert(counting(N)),
          even_worse.
      
      even_worse :-
          retract(counting(0)).
      even_worse :-
          retract(counting(N)),
          N > 0,
          N1 is N - 1,
          assert(counting(N1)),
          even_worse.
    • Passing the counter as an argument:

      %! \begin{focus}
      good_count(0).
      good_count(N) :-
          N > 0,
          N1 is N - 1,
          good_count(N1).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
      Try:

      ?- speedup(bad_count(10000), good_count(10000)).
      ?- speedup(bad_count(100000), good_count(100000)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).
    good_count(10000) is over 40 times faster than bad_count(10000), and uses virtually no memory.

Database (Contd.)

  • Asserting results which have been found true (lemmas).
    Example (real executions):

    • Direct definition of the Fibonacci sequence:

      :- module(_,_).
      
      fib(0, 0).
      fib(1, 1).
      fib(N, F) :-
          N > 1,
          N1 is N - 1,
          N2 is N1 - 1,
          fib(N1, F1),
          fib(N2, F2),
          F is F1 + F2.
    • Asserting results that have been found true (lemmas):

      :- dynamic lemma_fib/2.
      
      lemma_fib(0, 0).
      lemma_fib(1, 1).
      
      lfib(N, F) :-
          lemma_fib(N, F),
          !.
      lfib(N, F) :-
          N > 1,
          N1 is N - 1,
          N2 is N1 - 1,
          lfib(N1, F1),
          lfib(N2, F2),
          F is F1 + F2,
          assert(lemma_fib(N, F)).
    • To measure lfib/2 from scratch, the table is emptied first:

      %! \begin{focus}
      lfib_fresh(N, F) :-
          retractall(lemma_fib(_, _)),
          assert(lemma_fib(0, 0)),
          assert(lemma_fib(1, 1)),
          lfib(N, F).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
      Try:

      ?- speedup(fib(24,_), lfib_fresh(24,_)).
      The speedup grows with N:

      ?- speedup(fib(30,_), lfib_fresh(30,_)).
      If the table is not emptied, lfib/2 just finds the result in it:

      ?- speedup(fib(24,_), lfib(24,_)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).

    lfib(24, F) is several hundred times faster than fib(24, F), and uses over 1000 times less memory (and almost no time if called again).

    Warning: only useful when intermediate results are reused.

Determinism (I)

  • Many problems are deterministic.
  • Non-determinism is
    • Useful (automatic search).
    • But expensive.
  • Suggestions:

    • Do not keep alternatives if they are not needed:

      :- module(_,_).
      :- use_module(library(between)).
      
      member_check([X|_],X) :- !.
      member_check([_|Xs],X) :- member_check(Xs,X).
      Try (asking for more solutions):

      ?- member(b,[a,b,c,b]).
      ?- member_check([a,b,c,b],b).
    • Program deterministic problems in a deterministic way:

      decomp_simplistic(N, S1, S2) :-
          between(0, N, S1),
          between(0, N, S2),
          N =:= S1 + S2.
      
      decomp(N, S1, S2) :-
          between(0, N, S1),
          S2 is N - S1.
      Try (asking for more solutions):

      ?- decomp_simplistic(3,S1,S2).
      ?- decomp(3,S1,S2).
    • The speedup, finding all the solutions (it grows with N):

      %! \begin{focus}
      all(Goal) :-
          call(Goal),
          fail.
      all(_).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
      ?- speedup(all(decomp_simplistic(1000,_,_)), all(decomp(1000,_,_))).
      ?- speedup(all(decomp_simplistic(3000,_,_)), all(decomp(3000,_,_))).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).

Determinism (II)

  • Checking that two (ground) lists contain the same elements

    • Checking that two (ground) lists contain the same elements:

      Naive:

      :- module(_,_).
      
      same_elements_naive(L1, L2) :-
          \+ (member(X, L1), \+ member(X, L2)),
          \+ (member(X, L2), \+ member(X, L1)).
      Sort and unify (sorting can be done in O(N log N)):

      same_elements(L1, L2) :-
          sort(L1, Sorted),
          sort(L2, Sorted).
      Try:

      ?- same_elements_naive([a,b,c],[c,a,b]).
      ?- same_elements([a,b,c],[c,a,b]).
    • The speedup, with lists of N elements in scrambled order (it grows with N):

      test(N) :-
          scrambled(N, L),
          reverse(L, R),
          speedup(same_elements_naive(L,R), same_elements(L,R)).
      ?- test(1000).
      ?- test(5000).
      (The numbers 1 to N, scrambled: N must not be a multiple of 7919.)

      %! \begin{focus}
      scrambled(N, L) :-
          scrambled_from(0, N, L).
      
      scrambled_from(N, N, []) :- !.
      scrambled_from(I, N, [X|Xs]) :-
          X is (I * 7919) mod N + 1,
          I1 is I + 1,
          scrambled_from(I1, N, Xs).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).
  • With 1000 elements sort and unify is several hundred times faster, and the speedup grows with the size of the lists.

Indexing

  • Indexing on the first argument:
    • At compile time an indexing table is built for each predicate based on the principal functor of the first argument of the clause heads
    • At run-time only the clauses with a compatible functor in the first argument are considered
  • Result: appropriate clauses are reached faster and choice-points are not created if there are no "eligible" clauses left
  • Improves the ability to detect determinacy, important for preserving working storage

Indexing (Contd.)

  • Example: value greater than all elements in list

    • Value greater than all elements in list, without using indexing:

      :- module(_,_).
      
      bad_greater(_X,[]).
      bad_greater(X,[Y|Ys]) :- X > Y, bad_greater(X,Ys).
    • Same, making use of first-argument indexing:

      good_greater([],_X).
      good_greater([Y|Ys],X) :- X > Y, good_greater(Ys,X).
      Try:

      ?- bad_greater(5,[1,2,3]).
      ?- good_greater([1,2,3],5).
    • The speedup, with a list of N elements:

      test(N) :-
          numbers(N, L),
          X is N + 1,
          speedup(bad_greater(X,L), good_greater(L,X)).
      ?- test(600000).
      %! \begin{focus}
      numbers(0, []).
      numbers(N, [N|Ns]) :-
          N > 0,
          N1 is N - 1,
          numbers(N1, Ns).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).
    With 600,000 elements, good_greater/2 is about twice as fast.
  • Can be used with structures other than lists
  • Available in most Prolog systems

Iteration vs. Recursion

  • When the recursive call is the last subgoal in the clause and there are no alternatives left in the execution of the predicate, we have an iteration
  • Much more efficient
  • Example:

    • Recursive sum of a list:

      :- module(_,_).
      
      sum([], 0).
      sum([N|Ns], Sum) :-
          sum(Ns, Inter),
          Sum is Inter + N.
    • Iterative version (tail recursive, using an accumulator):

      sum_iter(L, Res) :-
          sum_acc(L, 0, Res).
      
      sum_acc([], Res, Res).
      sum_acc([N|Ns], In, Out) :-
          Inter is In + N,
          sum_acc(Ns, Inter, Out).
      Try:

      ?- sum([1,2,3],S).
      ?- sum_iter([1,2,3],S).
    • The speedup, with a list of N elements:

      test(N) :-
          numbers(N, L),
          speedup(sum(L,_), sum_iter(L,_)).
      ?- test(100000).
      %! \begin{focus}
      numbers(0, []).
      numbers(N, [N|Ns]) :-
          N > 0,
          N1 is N - 1,
          numbers(N1, Ns).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).
    With 100,000 elements, sum_iter/2 is about 2.5 times faster than sum/2.

Iteration vs. Recursion (Contd.)

  • The basic skeleton is:

    <head>:- 
        <deterministic computation>
        <recursive_call>.
  • Known as tail recursion
  • Particular case of last call optimization
  • It also consumes less memory

Cuts

  • Cuts eliminate choice–points, so they "create" determinism
  • Example:

    a:-  
     test_1, !, 
     ...
    a:- 
     test_2, !, 
     ...
    ...
    a:- 
     test_n, !, 
     ...
  • If mutually exclusive, declarative meaning of program not affected.
  • Otherwise, be careful: Declarativeness, Readability.

Delaying Work

  • Do not perform useless operations
  • In general:
    • Do not do anything until necessary
    • Put the tests as soon as possible
  • Performing the arithmetic before the tests:

    :- module(_,_).
    
    x2x3([], []).
    x2x3([X|Xs], [NX|NXs]) :-
        NX is -X * 2,
        X < 0,
        x2x3(Xs, NXs).
    x2x3([X|Xs], [NX|NXs]) :-
        NX is X * 3,
        X >= 0,
        x2x3(Xs, NXs).
  • Delaying the arithmetic operations:

    x2x3_1([], []).
    x2x3_1([X|Xs], [NX|NXs]) :-
        X < 0,
        NX is -X * 2,
        x2x3_1(Xs, NXs).
    x2x3_1([X|Xs], [NX|NXs]) :-
        X >= 0,
        NX is X * 3,
        x2x3_1(Xs, NXs).
    Try:

    ?- x2x3([1,-2,3],L).
    ?- x2x3_1([1,-2,3],L).
  • The speedup, with a list of N positive and negative numbers (N even):

    test(N) :-
        numbers(N, L),
        speedup(x2x3(L,_), x2x3_1(L,_)).
    ?- test(100000).
    %! \begin{focus}
    numbers(0, []).
    numbers(N, [N,M|Ns]) :-
        N > 0,
        M is -N,
        N1 is N - 2,
        numbers(N1, Ns).
    %! \end{focus}
    speedup(Slow, Fast) :-
        speedup_loop(true, Slow, Fast).
    
    speedup_loop(Loop, Slow, Fast) :-
        time_of(Loop, Slow, TS),
        time_of(Loop, Fast, TF),
        show_speedup(TS, TF).
    
    time_of(Loop, Goal, T) :-
        repeat_time(1, Loop, Goal, T).
    
    repeat_time(N, Loop, Goal, T) :-
        loop_time(N, Goal, TG),
        (   TG >= 100
        ->  loop_time(N, Loop, TL),
            (   TG - TL > TG / 10        % (else too fast to tell
            ->  T is (TG - TL) / N       %  from the loop itself)
            ;   T = 0
            )
        ;   N1 is N * 10,
            repeat_time(N1, Loop, Goal, T)
        ).
    
    loop_time(N, Goal, T) :-
        statistics(runtime, [T0, _]),
        ( between(1, N, _), once(Goal), fail ; true ),
        statistics(runtime, [T1, _]),
        T is T1 - T0.
    
    show_speedup(TS, TF) :-
        format("Slow: ", []), show_time(TS),
        format(", fast: ", []), show_time(TF),
        (   TS > 0, TF > 0
        ->  S is TS / TF,
            format(", speedup: ~1f~n", [S])
        ;   nl
        ).
    
    show_time(T) :-
        T > 0, !,
        format("~4f ms", [T]).
    show_time(_) :-
        format("too fast to measure", []).
    
    :- use_module(library(between)).
  • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).

With 100,000 elements, delaying the arithmetic is about 1.2 times faster.

Delaying Work

  • Delaying head unification + determinism:

    • The first version, from the previous slide:

      :- module(_,_).
      
      x2x3([], []).
      x2x3([X|Xs], [NX|NXs]) :-
          NX is -X * 2,
          X < 0,
          x2x3(Xs, NXs).
      x2x3([X|Xs], [NX|NXs]) :-
          NX is X * 3,
          X >= 0,
          x2x3(Xs, NXs).
    • Delaying head unification and adding determinism:

      x2x3_2([], []).
      x2x3_2([X|Xs], Out) :-
          X < 0, !,
          NX is -X * 2,
          Out = [NX|NXs],
          x2x3_2(Xs, NXs).
      x2x3_2([X|Xs], Out) :-
          X >= 0, !,
          NX is X * 3,
          Out = [NX|NXs],
          x2x3_2(Xs, NXs).
      Try:

      ?- x2x3_2([1,-2,3],L).
    • The speedup, with a list of N positive and negative numbers (N even):

      test(N) :-
          numbers(N, L),
          speedup(x2x3(L,_), x2x3_2(L,_)).
      ?- test(100000).
      %! \begin{focus}
      numbers(0, []).
      numbers(N, [N,M|Ns]) :-
          N > 0,
          M is -N,
          N1 is N - 2,
          numbers(N1, Ns).
      %! \end{focus}
      speedup(Slow, Fast) :-
          speedup_loop(true, Slow, Fast).
      
      speedup_loop(Loop, Slow, Fast) :-
          time_of(Loop, Slow, TS),
          time_of(Loop, Fast, TF),
          show_speedup(TS, TF).
      
      time_of(Loop, Goal, T) :-
          repeat_time(1, Loop, Goal, T).
      
      repeat_time(N, Loop, Goal, T) :-
          loop_time(N, Goal, TG),
          (   TG >= 100
          ->  loop_time(N, Loop, TL),
              (   TG - TL > TG / 10        % (else too fast to tell
              ->  T is (TG - TL) / N       %  from the loop itself)
              ;   T = 0
              )
          ;   N1 is N * 10,
              repeat_time(N1, Loop, Goal, T)
          ).
      
      loop_time(N, Goal, T) :-
          statistics(runtime, [T0, _]),
          ( between(1, N, _), once(Goal), fail ; true ),
          statistics(runtime, [T1, _]),
          T is T1 - T0.
      
      show_speedup(TS, TF) :-
          format("Slow: ", []), show_time(TS),
          format(", fast: ", []), show_time(TF),
          (   TS > 0, TF > 0
          ->  S is TS / TF,
              format(", speedup: ~1f~n", [S])
          ;   nl
          ).
      
      show_time(T) :-
          T > 0, !,
          format("~4f ms", [T]).
      show_time(_) :-
          format("too fast to measure", []).
      
      :- use_module(library(between)).
    • speedup/2 runs the two goals and compares their times, as in the first example (010_swap.pl).
    About 1.4 times faster than the first version (and under a third of the memory).

Conclusions

  • Avoid inheriting programming styles from other languages
  • Program in a declarative way:
    • Improves readability
    • Allows compiler optimizations
  • Avoid using the dynamic database when possible
  • Look for deterministic computations when programming deterministic problems
  • Put tests as soon as possible in the program (early pruning of the tree)
  • Delay computations until needed

  • Final thought: learning Prolog implementation techniques (e.g., the Warren Abstract Machine) is very instructive and useful. See the available slides and book on the topic.