Term_attvars/2 vs call_residue_vars/2

I have some code that uses when/2 to allow “bi-directional” predicates. When the computation is finished, there shouldn’t be an “frozen” goals, so I do something like this (the actual code is more complex, but I think this captures the gist of it):

pred(X,Y) :-
    maplist(pred_on_subterm, X, Y),
    ( term_attvars((X,Y), []) -> true ; instantiation_error((X,Y)) ).
...
pred_on_subterm(A, B) :-
    when((nonvar(A);nonvar(B)), pred_on_subterm_impl(A, B))

But alternatively, I could wrap the call with call_residue_vars/2.

All potential “frozen” variables should be reachable from the top-level X and Y, so I think the effect of both term_attvars/2 and call_residue_vars/2 would be the same but term_attvars/2 should be faster. Is this a correct understanding?

I guess both ways are mostly for debugging. Both are potentially expensive. call_residue_vars/2 is particularly expensive as it watches for constrained variables that are not connected to any particular variable in some specific term. This provides a sound guarantee nothing suspicious is going on. The price is to disable garbage collection for attributed variables.

All in all, I have my doubt using when/2 to avoid the need for explicit checks on modes and acting accordingly is a great idea … It looks elegant. It isn’t generally performing too well and if you need as post-check for attvars this gets even worse while attvars not introduced by the code being checked may be completely sound.

It does make my code simpler, especially in situations where I have multiple predicates that need to be re-ordered – using var/1 instead of when/2 results in a combinatoric explosion of alternatives.

I wonder if the cost of when/2 shows up when running profile/1? (Also, can I assume that the cost of when(nonvar(X), p(X)) is low if X is instantiated?)

(BTW, in this particular situation, I could probably replace the term_attvars(X,[]) by ground(X).)

profile/1 is your friend :slight_smile:

I doubt that makes a huge difference. Both need to process the entire term (assuming they are meant to succeed).

I was wondering whether profile/1 would show the effects of when/2 overhead or if it’d be lumped together with other aspects of the Prolog engine. (And I first need to make some non-micro test cases, both to provide enough granularity and to see if performance is even an issue.)

You’ll recognize it in the profile. It shows up as ‘$wakeup’ for the delayed calls and predicates from the when module in the case there is no delay.

1 Like

It appears that using when/2 adds ~7% overhead to my code (using profile/1), possibly less. There are larger inefficiencies in the code than that, so for now, I’ll stick with the simpler code that when/2 allows.