Reducing the overkill of setup_call_cleanup/3

There is an interesting pattern emerging. Usually code
is written as follows, for example:

setup_call_cleanup(
    open(Path, read, Input),
    read_term(Input, Term, Options),
    close(Input))

But then here and then one finds the following variant.
This is often practiced by Logtalk code, maybe because
setup_call_cleanup/3 is not available everywhere:

open(Path, read, Input),
catch(read_term(Input, Term, Options), Error,
        (close(Input),throw(Error))),
close(Input)

Using the alternative idiom is viable when the called goal
is deterministic, so that the cleanup is called anyway immediately
and no continuation handling is needed.

Some question:

  • Would it make sense to put the Logtalk pattern into
    a library and give it a meta-predicate wrapping?
    Which one performs better?

  • How would we solve that the Logtalk pattern has a small
    defect, i.e. an interrupt might happen before the catch/3, so
    that the cleanup isn’t called.

1 Like

I think the idea should be to use setup_call_cleanup/3 as the “standard” and implement that as good as you can on each system. The origin comes (AFAIK) from SICStus that has call_cleanup/2. This was considered insufficient as interrupts may happen between creating the resource and entering the call_cleanup/2.

ISO Prolog just doesn’t have a answer to this. Ok, it also does not defined interrupts, so that is solved. Still, getting resources cleaned regardless of how the accessing predicate finishes cannot be done. Think of the reasons that setup_call_catcher_cleanup/4 provides. You can deal with an exception in the goal as well as success or failure. However, you cannot deal with non-determinism.

I infrequently use setup_call_catcher_cleanup/4, mainly in cases where I do not want to cleanup in case of success.

Performance-wise, the call_cleanup/2 family is about the same as catch/3. The implementations share a lot of code.

The only sensible alternative is to leave resource cleanup to GC. That was proposed by Joachim if I recall correctly. The downside is that notably file handles come in limited numbers and sometimes need to be closed before we can do the next step (notably on Windows to avoid sharing violations). As I have discovered, GC also comes with the problem that in case of an error there is no good place for delivering the exception. SWI-Prolog currently allows closing streams by GC if the flag agc_close_streams is set to true. It is mostly useful to discover that your application does not close all streams.

1 Like

Pulling this out separately so that it can be liked.


Knew of setup_call_catcher_cleanup/4 for a long time but never really understood when it should be used. Now it makes more sense.

I see. You probably get closer results in case of an exception :slight_smile: Yes, the case where goal succeeds deterministically is probably faster using catch/3. That can probably be improved. Calling the cleanup handler is fairly complicated if the cleanup is due to “external” cuts or exceptions, i.e., the goal succeeds with a choicepoint and this is destroyed due to a ! or exception. The current implementation uses the same route for all scenarios while it could probably use a faster route for the normal success or failure of goal. setup_call_cleanup/3 is also based on setup_call_catcher_cleanup/4 and thus unifies the reason for the cleanup. Stripping that should also save a few cycles. Making setup_call_cleanup/3 faster seems a better route than replacing it with something unsafe. findall/3 with a fast generator and just a couple solutions is probably one of the few cases where this matters.

And it also matters for Prolog systems that don’t have a working
setup_call_cleanup/3. Currently setup_call_cleanup/3 is buggy in
Tau Prolog and Scryer Prolog. And its missing in Ciao Prolog.

That it is probably faster could be a motivation to adopt the Logtalk idiom
here and then by other Prolog systems, that have setup_call_cleanup/3,
unless setup_call_cleanup/3 can be made faster.

But I don’t think that setup_call_cleanup/3 can be made faster, because when
it detects that its goal argument is deterministic, it has already wasted time
in some bookkeeping for the case when the goal is non-deterministic.

You could only make it faster with some Mercury compilation techniques,
that would infer some determinism information at compile time, and
not figure out determinism only at runtime.

Edit 14.02.2023
But catch/3 has a similar bookkeeping problem. So why is catch/3
faster? Maybe it has to do with the atomicity of the setup. Here is a
proposal how to extend the Logtalk idiom by atomicity:

shield(
    open(Path, read, Input),
    catch(unshield(read_term(Input, Term, Options)), Error,
        (close(Input),throw(Error))),
    close(Input)).

shield/1 is a new meta predicate inspired by Python:

awaitable asyncio.shield(aw )
Protect an awaitable object from being cancelled.
https://docs.python.org/3/library/asyncio-task.html#shielding-from-cancellation

I guess it corresponds to sig_atomic/1 in SWI-Prolog and sys_atomic/1
in formerly Jekejeke Prolog. unshield/1 reverts shield/1, I didn’t find it yet
in some existing Prolog system. Disclaimer: The above idea of a

shielded Logtalk idiom is not yet practically tested.

The “Logtalk idiom” (not sure why you call it that way, it is merely “as good as it gets if you want to stay in the ISO world” is wrong if you allow for interrupts. Even if you don’t, it is only fairly relaxed if the goal that relies on the resource is deterministic. If you want support for semi-det, it already becomes hairy and nondet can’t be done within the ISO paradigm (well, ISO is Turing complete, so I guess using a meta interpreter it can be done :slight_smile: ).

SICStus introduced call_cleanup/2, which is the hard part of setup_call_cleanup/3, for a reason … And yes, it is not easy to implement correctly :frowning:

The main performance issue is due to the fact that the cleanup handler is called as a callback from C. This is hard to avoid if the cleanup is due to an external ! or exception. In case of success or failure we can probably just inject the cleanup goal in the current Prolog environment. The required test is unlikely the cause of the slowdown.

The full name would be “Logtalk idiom for deterministic finally”.
In imperative languages one can use either catch/throw:

try {
   call();
} catch (E) {
   cleanup();
   throw E;
}
cleanup();

Or finally, if its available:

try {
   call();
} finally {
   cleanup();
}

Why use Logtalk in the name of the idiom. Its similar like
Neanderthals are named after the Neander Valley , where they
were found. If you show me some Quintus code snippet that

uses the above idiom, we could also name it Quintus idiom.
But now that we are talking about it. Maybe calling it the
Neanderthal idiom, could be also fun, because it is less

sophisticated than the setup_call_cleanup/3 proposal.

Edit 14.02.2023
Adding failure handling to it, wouldn’t be extremly difficult,
but maybe this would slow it down a little bit? Something
along the following, for the version without shield/1:

open(Path, read, Input),
(catch(read_term(Input, Term, Options), Error,
        (close(Input),throw(Error))) -> close(Input)
 ;      close(Input))

Maybe a good name for this less sophisticated setup_call_cleanup/3
would be simply setup_once_cleanup/3. The once in the name
would relate to the already existing once/1 predicate and indicate that

the call is only called once. Which happens for example if the above
if-then-else wrapper is used. In the original Logtalk find, the cleanup
might not be called or multiple times called, since there is no

if-then-else wrapping. A new meta predicate setup_once_cleanup/3
could provide a more cleaner solution.

I agree. On ISO systems you can implement such a predicate which handles a fair deal of the use cases. Systems with interrupts require setup_call_cleanup/3, which can be used to bootstrap setup_once_cleanup/3.

Seems SICStus still only has call_cleanup/2. This is a bit of a surprise as they do have timeouts. I’d assume these can happen between the resource creation and guarding it using call_cleanup/2.

There are a variety of interrupts that can play into setup_call_cleanup/3 and
friends. Its not only that a timeout might interrupt a Prolog program. Yesterday
I saw that Scryer Prolog doesn’t give a prompt upon Ctrl-C, but directly returns

to the top-level, which is just another variant top-level behaviour:

$ target/release/scryer-prolog -v
"v0.9.1-160-g56783b8e"
$ target/release/scryer-prolog
?- repeat, fail.
^C   error('$interrupt_thrown',repl/0).

From another Prolog system documentation, SICStus, it seems they account
for SIGINT or SIGBREAK from keyboard and SIGVTALRM from timeout.
How they make the call_cleanup/2 robust, I don’t know. I didn’t find some

sig_atomic/1 either. That a robust call_cleanup/2 is needed is only seen
when somebody really makes use of timeout or other interrupts. I am also
using an interrupt in my concurrent_and/2 implementation. One then quickly

runs into scenarios where a non robust call_cleanup/2 causes problems.

Real-world programming needs timeouts and interrupts IMO. Pushed some enhancements to the cleanup family and findall/3 that reduces the overhead a little. For 1M simple findall/3 calls from about 0.9 to 0.7 sec. The bottom line seems to be about 0.52 (copying findall/3 and simply remove all guarding of the cleanup).

SWI-Prolog does have a fast path for call/1 if the called thing is a simple predicate. But still, we have a (goal) term, we must get its functor, we must find relevant module, do a hash lookup for the predicate, verify it is something we can call using the fast lane, create a new stack frame, fill the arguments and then call.

For most of it, it doesn’t matter too much that setup_call_cleanup/3 isn’t blindly fast. The most common “resource” used with this construct is a stream. In this whole picture the overhead is neglectable. findall/3 is one of the exceptions. Preparing to collect solutions and cleaning this are pretty well optimized, so if the goal is really fast the setup_call_cleanup/3 has a significant impact and this is not so uncommon. Think about exploring a graph using bread-first search.