8

I'm trying to sort a 10k element list in prolog with bubblesort and I get the out of local stack error. Mergesort seems to be the best option since I don't get any errors for the same input. However I'd really like to get some running times for bubblesort with large input data but I can't. Any ideas?

Here's the code:

 %% NOTE: SWI-PROLOG USED

 %% generate_list(Limit, N, L): - insert upper limit and length of list N
 %% to get a random list with N numbers from 0 to limit
 generate_list(_, 0, []).
 generate_list(Limit, N, [Y|L]):-
    N =\= 0,
    random(0, Limit, Y),
    N1 is N-1,
    generate_list(Limit, N1, L).


 %% bubble(L, Ls, max):- insert list L and get max member of list by
 %% swapping members from the start of L.
 bubble([Z], [], Z).
 bubble([X,Y|L], [X|Ls], Z):- X =< Y, bubble([Y|L], Ls, Z).
 bubble([X,Y|L], [Y|Ls], Z):- X  > Y, bubble([X|L], Ls, Z).

 %% bubble_sort(List, Accumulator, Sorted_List)
 bubblesort([X], Ls, [X|Ls]).
 bubblesort(L, Accumulate, Result):- bubble(L, Ls, Max),
       bubblesort(Ls, [Max|Accumulate], Result).

 bubble_sort(L, Sorted):- bubblesort(L, [], Sorted).

As you can I see I'm using tail recursion. I've also tried enlarging the stacks by using:

  set_prolog_stack(global, limit(100 000 000 000)).
  set_prolog_stack(trail,  limit(20 000 000 000)).
  set_prolog_stack(local,  limit(2 000 000 000)).

but it just runs for a bit longer. Eventually I get out of local stack again. Should I use another language like C and malloc the list or not use recursion?

repeat
  • 18,496
  • 4
  • 54
  • 166
Manos Ntoulias
  • 513
  • 1
  • 4
  • 21
  • 4
    Tail recursion optimization does not help in your case because your predicate is not deterministic: Argument indexing cannot tell the different clauses of `bubble/3` apart. You can rewrite the clauses in such a way (introduce an auxiliary predicate and lagged argument) that they can be distinguished by first argument indexing, without sacrificing purity or monotonicity. Do not be tempted to introduce any impurities! Instead use for example `compare/3` and an auxiliary predicate where `>`, `<`, or `=` is the first argument. Such a solution can later be generalized easily (CLPFD). – mat Mar 20 '15 at 00:01

3 Answers3

7

Since there are two answers, and no one pointed out explicitly enough the reason why you get into "out of local stack" trouble (Mat says in the comment to your question that your predicates are not deterministic, but does not explain exactly why).

Two of the predicates you have defined, namely, bubblesort/3 and bubble/3, have mutually exclusive clauses. But Prolog (at least SWI-Prolog) does not recognize that these are mutually exclusive. So, choice points are created, you don't get tail recursion optimization, and probably no garbage collection (you need to measure using your implementation of choice if you want to know how much goes where and when).

You have two different problems.

Problem 1: lists with exactly one element

This problem pops up in both predicates. In the most simple predicate possible:

foo([_]).
foo([_|T]) :-
    foo(T).

And then:

?- foo([a]).
true ;
false.

This is not surprising; consider:

?- [a] = [a|[]].
true.

You can solve this by using a technique called lagging:

bar([H|T]) :-
    bar_1(T, H).
bar_1([], _).
bar_1([H|T], _) :-
    bar_1(T, H).

Then:

?- bar([a]).
true.

In the definition of bar_1/2, the first argument to the first clause is the empty list; the first argument to the second clause is a non-empty list (a list with at least one element, and a tail). Prolog does not create choice points when all clauses are obviously exclusive. What obvious means will depend on the implementation, but usually, when the first arguments to all clauses are all terms with different functors, then no choice points are created.

Try the following (you might get different results, but the message is the same):

?- functor([], Name, Arity).
Name = [],
Arity = 0.

?- functor([_|_], Name, Arity).
Name = '[|]',
Arity = 2.

See this question and the answer by Mat to see how you can use this to make your program deterministic.

Mat, in his answer, uses this approach, if I see correctly.

Problem 2: constraints (conditions) in the body of the clauses

This is the problem with the second and third clause of bubble/3. In the textbook "correct" example of choosing the minimum of two elements:

min(A, B, B) :- B @< A.
min(A, B, A) :- A @=< B.

Then:

?- min(1,2,1).
true.

but:

?- min(2,1,1).
true ;
false.

You can solve this in two ways: either by doing what Mat is doing, which is, using compare/3, which succeeds deterministically; or, by doing what CapelliC is doing, which is, using an if-then-else.

Mat:

min_m(A, B, Min) :-
    compare(Order, A, B),
    min_order(Order, A, B, Min).
min_order(<, A, _, A).
min_order(=, A, _, A).
min_order(>, _, B, B).

And Carlo:

min_c(A, B, Min) :-
    (   B @< A
    ->  Min = B
    ;   Min = A
    ).

I know there will always be at least as many opinions as heads, but both are fine, depending on what you are doing.

PS

You could use the built in length/2 to generate a list, and re-write your generate_list/3 like this:

generate_list(Limit, Len, List) :-
    length(List, Len),
    random_pos_ints(List, Limit).

random_pos_ints([], _).
random_pos_ints([H|T], Limit) :-
    random(0, Limit, H),
    random_pos_ints(T, Limit).

The helper random_pos_ints/2 is a trivial predicate that can be expressed in terms of maplist:

generate_list(Limit, Len, List) :-
    length(List, Len),
    maplist(random(0, Limit), List).    
Community
  • 1
  • 1
  • Thanks. However i still don't understand why choice points aren't created in the bar predicate? Shouldn't after success still check the bar_1([H|T], _) :- bar_1(T, H) clause? I tried searching for this technique but didn't find much – Manos Ntoulias Mar 21 '15 at 16:31
  • @user3161227 I will add this to the answer... You can also try to make the title of you question a bit more general: at least add "avoiding choice point creation" or something of the sort –  Mar 21 '15 at 16:46
5

Here is a version of bubble/3 that is deterministic if the first argument is instantiated, so that tail call optimisation (and, more specifically, tail recursion optimisation) applies:

bubble([L|Ls0], Ls, Max) :- phrase(bubble_(Ls0, L, Max), Ls).

bubble_([], Max, Max) --> [].
bubble_([L0|Ls0], Max0, Max) -->
        elements_max(L0, Max0, Max1),
        bubble_(Ls0, Max1, Max).

elements_max(X, Y, Max) -->
        { compare(C, X, Y) },
        c_max(C, X, Y, Max).

c_max(<, X, Y, Y) --> [X].
c_max(=, X, Y, Y) --> [X].
c_max(>, X, Y, X) --> [Y].

Example usage, with the rest of the program unchanged (running times depend on the random list, which is bad if you want to reproduce these results - hint: introduce the random seed as argument to fix this):

?- generate_list(100, 10_000, Ls), time(bubble_sort(Ls, Ls1)).
% 200,099,991 inferences, 29.769 CPU in 34.471 seconds
...

For testing different versions, please use a version of the query that you can use to reliably reproduce the same initial list, such as:

?- numlist(1, 10_000, Ls0), time(bubble_sort(Ls0, Ls)).

The nice thing is: If you just use zcompare/3 from library(clpfd) instead of compare/3, you obtain a version that can be used in all directions:

?- bubble(Ls0, Ls, Max).
Ls0 = [Max],
Ls = [] ;
Ls0 = [Max, _G677],
Ls = [_G677],
_G677#=<Max+ -1,
zcompare(<, _G677, Max) ;
Ls0 = [Max, _G949, _G952],
Ls = [_G949, _G952],
_G952#=<Max+ -1,
_G949#=<Max+ -1,
zcompare(<, _G952, Max),
zcompare(<, _G949, Max) ;
etc.

This describes the relation in general terms between integers.

mat
  • 40,498
  • 3
  • 51
  • 78
  • I get much slower execution times than you showed (i.e. `?- generate_list(100,2000,L),time(bubble_sort(L,S)). % 329,209,251 inferences, ...`). Did you changed something not shown in the answer ? – CapelliC Mar 20 '15 at 08:32
  • As I said: the execution time depends on the *random list* that is generated. To reproduce the result, you must be very lucky and generate exactly the same list that was generated in my case. Please try it with a deterministic test case with the same list for both versions to compare them. – mat Mar 20 '15 at 09:08
  • I've posted the comparison code at http://swish.swi-prolog.org/p/cmp_bubble_sorts.pl, this query shows the problem: `?- generate_list(100,200,L),time(bubble_sort_m(L,S)),time(bubble_sort_c(L,C)).` – CapelliC Mar 20 '15 at 09:40
  • 1
    @CapelliC You are using `zcompare` in the code on swish! Change it to `compare` and it's back to "normal" (faster due to lack of creating and removing choice points). –  Mar 20 '15 at 09:43
  • @CapelliC Another thing: I have already observed that for SWI-Prolog on my machine, sorting numbers with `min()` or `max()` or the arithmetic comparison operators is a bit _slower_ than using standard order of terms. But then again, using `compare/3` makes it impossible to enforce the list being made up of numbers only. –  Mar 20 '15 at 09:45
  • @Boris please let's have no more of this "numbers only" distinction, since `=<`, `>` etc. also are not limited to numbers only, but work on arbitrary arithmetic expression. If you want to enforce numbers, use `number/1` in both version. – mat Mar 20 '15 at 09:47
  • @Boris: thanks, I think that when I copy/pasted the code, there was zcompare/3. Indeed, the performance problem is solved used compare/3. – CapelliC Mar 20 '15 at 09:48
  • 1
    @CapelliC: yes, I accidentally posted the `zcompare/3` version itself, instead of the less general version. I have fixed this! – mat Mar 20 '15 at 09:49
  • @mat Sure, but this is how some of the library predicates (in SWI-Prolog) differentiate between a generic list and a list of (I stand corrected) arithmetic expression: by using arithmetic instead of term comparison. I apologize for bringing it up this one last time; it will be no more, as I do not wish to annoy anyone. –  Mar 20 '15 at 09:50
3

Disclaimer: following the hint by @mat could be more rewarding...

I've played a bit with your code, in my experiment the local stack overflow was thrown with a list length near 2500. Then I've placed some cut:

%% bubble(L, Ls, max):- insert list L and get max member of list by
%% swapping members from the start of L.
bubble([Z], [], Z).
bubble([X,Y|L], [R|Ls], Z):-
 ( X =< Y -> (R,T)=(X,Y) ; (R,T)=(Y,X) ),
 bubble([T|L], Ls, Z).

%% bubble_sort(List, Accumulator, Sorted_List)
bubblesort([X], Ls, [X|Ls]) :- !.
bubblesort(L, Accumulate, Result):-
 bubble(L, Ls, Max),
 !, bubblesort(Ls, [Max|Accumulate], Result).

and I get

?- time(generate_list(100,10000,L)),time(bubble_sort(L,S)).
% 60,000 inferences, 0.037 CPU in 0.037 seconds (99% CPU, 1618231 Lips)
% 174,710,407 inferences, 85.707 CPU in 86.016 seconds (100% CPU, 2038460 Lips)
L = [98, 19, 80, 24, 16, 59, 70, 39, 22|...],
S = [0, 0, 0, 0, 0, 0, 0, 0, 0|...] 
.

so, it's working, but very slowly, showing the quadratic complexity...

CapelliC
  • 59,646
  • 5
  • 47
  • 90
  • Following the hint by @mat to use `compare/3` is OK, but only if OP does _not_ care if he is _not_ sorting numbers only. It is difficult to know if this is the original intent. –  Mar 20 '15 at 07:01