Once this predicate is tabled, it loops forever

This is a bit off topic, but now that I read your code carefully enough, I noticed that you are doing something that is very much like this other thing, namely, raising to the power. I once shared the code, it is here:

So, if you wanted to not have to code gen_string_of_spaces/2 yourself, but still keep the same idea, it would go something like this:

:- use_module(power).

gen_string_of_spaces(N, Spaces) :-
    power(" ", N, string_concat, "", Spaces).

This is it. We are back to a one-liner, as with using format, and it has roughly the same hackiness index.

I have explained it in the original post, but briefly:

  • the first argument is the Element, in this case a string with one space;
  • the second argument is the power, here the length of the final string;
  • the third argument is the operation, here string_concat/3
  • the fourth argument is the neutral element, the empty string (if your N is 0)
  • the last argument is the result

I added the following two goals to your test suite:

goal_power(SzMax, Spaces) :-
    random_between(0, SzMax, RL),
    power(" ", RL, string_concat, "", Spaces).

goal_format(SzMax, Spaces) :-
    random_between(0, SzMax, RL),
    format(string(Spaces), "~t~*|", [RL]).

… and I also added them to the list of tested predicates:

test("generate strings of spaces of random length and drop them immediately after creation") :-
   maplist([Goal]>>create_and_drop(Goal),[goal_special,goal_direct,goal_power,goal_format]).

% SNIP

test("generate strings of spaces of random length and store them in a list") :-
   maplist([Goal]>>create_and_collect(Goal),[goal_special,goal_direct,goal_power,goal_format]).

Now, when I run your tests, I see:

% PL-Unit: stringy_performance 
% 3,712,138 inferences, 0.526 CPU in 0.526 seconds (100% CPU, 7063825 Lips)
drop them immediately (100000 calls) (100 max size) using goal 'goal_special': ""
% 11,194,928 inferences, 0.900 CPU in 0.901 seconds (100% CPU, 12433048 Lips)
drop them immediately (100000 calls) (100 max size) using goal 'goal_direct': ""
% 4,369,662 inferences, 0.536 CPU in 0.537 seconds (100% CPU, 8146089 Lips)
drop them immediately (100000 calls) (100 max size) using goal 'goal_power': ""
% 700,000 inferences, 0.313 CPU in 0.314 seconds (100% CPU, 2237162 Lips)
drop them immediately (100000 calls) (100 max size) using goal 'goal_format': ""
.
% 3,707,469 inferences, 0.649 CPU in 0.650 seconds (100% CPU, 5715648 Lips)
collect in list (100000 calls) (100 max size) using goal 'goal_special': ""
% 11,202,787 inferences, 1.316 CPU in 1.318 seconds (100% CPU, 8510957 Lips)
collect in list (100000 calls) (100 max size) using goal 'goal_direct': ""
% 4,367,630 inferences, 0.573 CPU in 0.574 seconds (100% CPU, 7623688 Lips)
collect in list (100000 calls) (100 max size) using goal 'goal_power': ""
% 700,001 inferences, 0.307 CPU in 0.308 seconds (100% CPU, 2278379 Lips)
collect in list (100000 calls) (100 max size) using goal 'goal_format': ""
. done
% All 2 tests passed
true.

The results are pretty similar between runs.

Please confirm my interpretation: it seems that using format(string(Spaces), "~t~*|", [N]) is still roughly twice as fast as the other approaches, with short strings? I suspect that doing less iteration but generating longer strings might make your initial implementation (and the “power”, which is basically the same) somewhat faster. I tried it with:

callcount(10000).
max_string_size(1000).

Indeed, now I get:

% PL-Unit: stringy_performance 
% 606,014 inferences, 0.095 CPU in 0.095 seconds (100% CPU, 6353769 Lips)
drop them immediately (10000 calls) (1000 max size) using goal 'goal_special': ""
% 10,019,866 inferences, 0.766 CPU in 0.767 seconds (100% CPU, 13079676 Lips)
drop them immediately (10000 calls) (1000 max size) using goal 'goal_direct': ""
% 647,857 inferences, 0.092 CPU in 0.092 seconds (100% CPU, 7050067 Lips)
drop them immediately (10000 calls) (1000 max size) using goal 'goal_power': ""
% 70,000 inferences, 0.153 CPU in 0.154 seconds (100% CPU, 456227 Lips)
drop them immediately (10000 calls) (1000 max size) using goal 'goal_format': ""
.
% 607,793 inferences, 0.122 CPU in 0.122 seconds (100% CPU, 4992397 Lips)
collect in list (10000 calls) (1000 max size) using goal 'goal_special': ""
% 10,156,645 inferences, 1.353 CPU in 1.354 seconds (100% CPU, 7506170 Lips)
collect in list (10000 calls) (1000 max size) using goal 'goal_direct': ""
% 649,262 inferences, 0.104 CPU in 0.105 seconds (100% CPU, 6213561 Lips)
collect in list (10000 calls) (1000 max size) using goal 'goal_power': ""
% 70,001 inferences, 0.155 CPU in 0.156 seconds (100% CPU, 450529 Lips)
collect in list (10000 calls) (1000 max size) using goal 'goal_format': ""
. done
% All 2 tests passed
true.

To be honest, I find this at least a bit surprising. I would have expected that using format is going to be always faster. I suspect that string_concat/3 is doing some trickery; I would have to read the implementation if I really wanted to know :slight_smile:

1 Like