5
$\begingroup$

I made a typo when entering code for Compile. I thought it should have resulted in an error when the code was compiled, but it didn't. It did give an error when the CompiledFunction was run, though. However, exploration revealed some interesting behavior, and I wonder if it is intentional.

The typo (m instead of ,):

cf1 = Compile[{{a, _Integer} m {b, _Integer}}, 1.]; (* no error when run *)
cf2 = Compile[{{a, _Integer} m {b, _Integer}}, a];  (* gives error when run *)

cf1[6.]
(*  1.  *)
cf2[6.]

CompiledFunction::cfse: Compiled expression a should be a machine-size real number.

CompiledFunction::cfex: Could not complete external evaluation at instruction 1; proceeding with uncompiled evaluation.

(*  a  *)

The a in the code for cf2 has syntax coloring suggesting that a is recognized as an argument. That is apparently in error (maybe a bug?). In any case, imagining why it compiled without error led me to try this, which works!:

cf3 = Compile[{{a, _Integer} m {b, _Integer}}, {a, _Integer} m {b, _Integer}];

cf3[6.]
(*  6.  *)

That led to the following and other experiments with argument declarations:

cf4 = Compile[{Sin[x^2]}, Sin[x^2]^2];

cf4[6.]
(*  36.  *)

It seems that any expression expr may be used, and if the body has the form f[expr], then the following would be equivalent:

Compile[{expr}, f[expr]]
Compile[{x}, f[x]]

Is this a feature of Compile? Is it documented? Can we safely use it in code generation? Or should it be considered a bug?


Clarification:

I thought it was obvious what was happening, but maybe I'm wrong, which I sometimes am concerning programming technicalities. I think the non-Symbol expression-variables are replaced in the body by the equivalent of a new Unique[]/Module[] variable (beginning with the SymbolName of the head of the expression, as @b3m2a1 notes). The following code does the replacement for cf6b, and one can inspect the CompilePrint output to see it is equivalent to original compiled function cf6a:

cf6 = Hold@
   Compile[{{Sin[x], _Integer}, {Sin[2 x], _Integer, 1}, {x, _Integer}}, 
    D[x + b Sin[x] + b^2 Sin[2 x], b] /. b -> 1.];
cf6a = ReleaseHold@cf6;
cf6b = cf6 /. cf : Hold@ Compile[v_, body_, opts___] :>
  (cf /. (Verbatim[#] -> Unique[SymbolName[Head[#]] <> "$", Temporary] & /@ 
     DeleteCases[_Symbol]@Replace[v, {sym_, __} :> sym, 1])) //
 ReleaseHold;
Needs["CompiledFunctionTools`"];
CompilePrint@cf6a
CompilePrint@cf6b
$\endgroup$
1
  • $\begingroup$ The reason the bottom two are equivalent is because only the Head of x and expr is used as a variable, if they aren't valid List argument specs. Dangerous game WRI is playing with that... $\endgroup$
    – b3m2a1
    Commented Oct 29, 2020 at 23:51

2 Answers 2

3
$\begingroup$

Here's what I think is happening. If we look at the CompilePrint for both:

CompilePrint[cf2]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        Result = R1

1   R1 = MainEvaluate[ Function[{Times$796906}, a][ R0]]
2   Return
"
CompilePrint[cf1]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        R1 = 1.
        Result = R1

1   Return
"

This tells us that the argument which is assume to be Real, is just absorbed into R0. Then we see that Times$796906 there, which comes from the Head wrapping the argument.

We can see what happens with a different Head:

cf3 = Compile[{Hold[{a, _Integer}, m, {b, _Integer}]}, a];

CompilePrint[cf3]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        Result = R1

1   R1 = MainEvaluate[ Function[{Hold$802124}, a][ R0]]
2   Return
"

It seems Mathematica is interpreting this construct like

Compile[{ singleArgument }, expr]

since that singleArgument doesn't fit into the form of a "regular" variable, Compile takes its Head and tries to force the function that will be sent to MainEvaluate to be side-effect free using that. This can be made clear by looking at


cf4 = Compile[{{a}}, b];
CompilePrint[cf4]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        Result = R1

1   R1 = MainEvaluate[ Function[{a}, b][ R0]]
2   Return
"

Same compiled form as for the other functions, but in this case since we just had a symbolic argument Global`a, we've got no issues.

We get interesting behavior if we use

cf5 = Compile[{a[1]}, b];
CompilePrint[cf5]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        Result = R1

1   R1 = MainEvaluate[ Function[{a$809738}, b][ R0]]
2   Return
"

where it's clear that Compile is doing some localization of (for some reason) just the Head of its argument.

My usual tricks and hacks aren't having the usual effect, so I can't determine if exactly that Function argument is just directly fed to the main loop, but I think it is. Maybe someone else can find a way around the internal implementation of Unique or whatever they're using. Here's what I've tried for that

cf6 =
  With[{m = $ModuleNumber},
   With[{b = ToExpression["a$" <> ToString[m]]},
Internal`InheritedBlock[
 {Unique},
 Block[{$ModuleNumber = m - 1},
      Unprotect[Unique];
      Unique[a] := b;
      Compile[
       {a[1]},
       b
       ]
      ]
     ]
    ]
   ];
CompilePrint[cf6]

"
        1 argument
        2 Real registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        R0 = A1
        Result = R1

1   R1 = MainEvaluate[ Function[{a$809750}, a$809751][ R0]]
2   Return
"
$\endgroup$
2
$\begingroup$

Well to be honest Compile works in very strange ways (in more then one regard). I do not think that this is intended and I have not seen it docummented or in any code I came across.

From some experiments I think the following is happening: Dropping the type specifier automatically assumes _Real and variable "names" can be (as OP discovered) rather exotic. The probable reason for this rather loose behavior is that arguments inside the CompileFunction are refereed to as A1 to AN (for a CompileFunction with N). The argument names specified by the user are not used in the final function. If one drops the outer curly bracket comma separated expression get treated as real scalar arguments. So

Needs["CompiledFunctionTools`"];
Compile[{Sin[x^2]},(Sin[x^2])^2];
%
%//CompilePrint

results in

Compiled function

with Compile[{{x, _Real}}, (x)^2]; as an equivalent conventional input form.

Somewhat scary in this context is this Compile[{x, _Real}, x + _Real]; which is equivalent to Compile[{{x, _Real},{y,_Real}}, x + y];. I do not know how robust this is and on first glance it seems rather useless to input functions/arguments this way but one advantage I see is the possibility to use strings, sup-/superscripted values and more for argument names which allows for names which are normally impossible in Mathematica. E.g.:

Compile[{{"A_1", _Real}, {"A_2", _Real}}, ("A_1")^2 + "A_2"];
Compile[{{Subscript[A, 1], _Real}, {Subscript[A,2], _Real}}, (Subscript[A, 1])^2 + Subscript[A, 2]]

work as one would expect. This might be a use case for this curious find.

I would call the whole scenario a "feature" in the sense that this seems to be rather robust behavior related to the input parser of compile. That being said there is no guarantee that the current behavior will persist across different versions of the software.

I always use CompilePrint to check the outputted CompiledFunction for obvious errors, unevaluated expressions (e.g. If[2==2,...]) and especially MainEvaluate[...] since in my experience (using compiled functions inside NDSolve) just one MainEvaluate[...] completely eliminates any performance benefit.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.