7
$\begingroup$

There seems to be a bug whereby Compile can deal with empty lists only when passed in some cases. Consider the case below.

comp1 = Compile[{},
   Join[{{0, 0}}, {}]];

comp1[]

(*output is {{0, 0}}*)

No error is thrown and all is well.

Now consider the following.

comp2 = Compile[{},
   Join[{{0, 0}}, ConstantArray[{0, 0}, 0]]];

comp2[]

(*output is {{0, 0}}*)

In this case, the following error is thrown.

error message

The outputs are rightfully the same, but it doesn't seem like the second case should throw an error and be uncompilable. Not only are ConstantArray[{0, 0}, 0] and {} equal; they are identical, as measured by SameQ.

ConstantArray[{0, 0}, 0] === {}

(*output is True*)

What is going on here?

I have code that requires the use of ConstantArray as above, but I want it to compile and not to throw an error. Are there any suggestions on how to deal with this?


Notes

  • My OS is macOS Big Sur Version 11.2.3
  • My Mathematica version is 12.1.1.0
$\endgroup$
7
  • $\begingroup$ MemberQ[Compile`CompilerFunctions[], ConstantArray] gives False , that is, ConstantArray is not compilable. Related: List of compilable functions $\endgroup$
    – kglr
    Sep 11 at 3:48
  • $\begingroup$ @kglr But it seems like ConstantArray is, in fact, compilable. comp = Compile[{}, Join[{}, ConstantArray[1, 1]]]; comp[] gives the expected output with no error, for example. This may be another issue. $\endgroup$ Sep 11 at 3:56
  • 1
    $\begingroup$ @kglr If you look further down on your link, it does include ConstantArray[] as compilable. $\endgroup$ Sep 11 at 4:55
  • 2
    $\begingroup$ If you read further down @kglr's link, to "Edit 2", you will see there are two meanings to "compilable." ConstantArray is not compilable in the first & most important sense, as is shown in WReach's answer (ConstantArray is evaluated via MainEvaluate, not in the compiled run-time environment WVM). $\endgroup$
    – Michael E2
    Sep 11 at 12:51
  • 2
    $\begingroup$ No, I don't. You can use Table instead. ConstantArray is a new function (2007); prior to that, one used Table. They're nearly interchangeable except for one usage: ConstantArray[c, {10, 10}, SparseArray]. While that usage would not be compilable, it is common that compiler versions of functions have restricted usages (restricted to numeric/boolean arrays, most obviously). $\endgroup$
    – Michael E2
    Sep 11 at 13:51
10
$\begingroup$

Root Cause

The root cause of the error is that we are attempting to join tensors of incompatible shapes. Namely, a 1x2 array...

Dimensions[{{0,0}}]
(* {1, 2} *)

... with a zero-length one-dimensional array:

Dimensions[ConstantArray[{0, 0}, 0]]
(* {0} *)

Dimensions[{}]
(* {0} *)

While this is permitted by the main evaluator, the compiler applies stricter type-checking rules. So when the compiled code is executed and this mismatch is detected then evaluation drops back to the main evaluator. This produces the correct result but issues a warning to indicate that the main evaluator was used unexpectedly.

Manifestly Empty Lists Optimized Out

But... if this is the case then why does comp1 not issue the warning message? The reason is that the compiler has optimized the Join expression when one of the arguments is explicitly an empty list:

Needs["CompiledFunctionTools`"]
CompilePrint[comp1]

compiled comp1 code

Notice how instruction 1 calls Join with a single argument. The second argument, the explicitly empty list, has been optimized out. (Interestingly, the Join is not optimized out when it is reduced to a single argument.)

ConstantArray Expressions Not Optimized Out

Now contrast this with the code of comp2:

CompilePrint[comp2]

compiled comp2 code

This time instruction 2 shows a two-argument call to Join. The compiler has not been able to recognize that the second argument will result in an empty list. That argument will be passed to the main evaluator to evaluate the ConstantArray expression.

Note that instruction 1 is expecting the result of the ConstantArray expression to be a two-dimensional tensor of integers (T(I2)). So it is surprised when the result turns out to be an empty one-dimensional list. The compiled code bails out and the Join expression is evaluated by the main evaluator (accompanied by a warning message due to the unplanned use of MainEvaluate for Join).

Different Reproduction Case

We can reproduce this behaviour by expressing the empty list by a different, fully-compilable expression:

comp3 = Compile[{}, Join[{{0, 0}}, {{0, 0}}[[;; 0]]]];

CompilePrint[comp3]

compiled comp3 code

Here again the compiler has not recognized that {{0, 0}}[[;; 0]] will be an empty list that could be optimized out of instruction 2. The Part expression is compiled, so unlike the previous example there is no explicit call to MainEvaluate. But this will also incur the type mismatch and fall back on the main evaluator implicitly for the Join (albeit with a slightly different warning message):

comp3[]

comp3 evaluation output

Workaround #1 - Explicit Guard

As far as I know, there is no way to represent a 2 x 0 matrix in Mathematica. Such an array always collapses into an empty one-dimensional list (e.g. Dimensions@Array[0, {0, 2}] === Dimensions@SparseArray[{}, {0, 2}] === Dimensions@Array[0, {0, 2, 3, 4}] === {0}).

So, assuming that we do not know a priori whether or not this situation is going to occur then we could introduce an explicit guard for the case. For example:

comp4 =
  Compile[{{n, _Integer}}
  , With[{a = {0, 0}}
    , If [n == 0, {a}, Join[{a}, ConstantArray[a, n]]]
    ]
  ];

comp4[1]
(* {{0, 0}, {0, 0}} *)

comp4[0]
(* {{0, 0}} *)

Workaround #2 - Compiler Type Trickery

Michael E2 notes in a comment that there does exist at least one compilation path in which the type system will preserve the two-dimensional structure of an empty array. It involves a combination of Most and Table:

comp5 = Compile[{{n, _Integer}}, Most@Table[{0, 0}, {n+1}]];

comp5[1]
(* {{0, 0}, {0, 0}} *)

comp5[0]
(* {{0, 0}} *)

This trick of preserving the 2x0 tensor structure only works in compiled code. The main evaluator still collapses Most@Table[{0, 0}, 1] into an empty one-dimensional list.

The compiled code for this variation is a bit more elaborate than the other versions as it involves explicit instructions to construct the temporary Table and drop the last element:

compiled comp5 code

This analysis is current as of Mathematica version 12.2.0.

$\endgroup$
5
  • 2
    $\begingroup$ FWIW, Compile[{}, Join[{{0, 0}}, Most@Table[{0, 0}, 1]]] runs without error or bailing out of the WVM. And Compile[{}, Dimensions@Most@Table[{0, 0}, 1]] returns {0,2}, which does not occur in the main evaluator of course.. $\endgroup$
    – Michael E2
    Sep 11 at 12:47
  • $\begingroup$ @MichaelE2 Nice find! I have updated my answer to include your suggestion. $\endgroup$
    – WReach
    Sep 11 at 15:28
  • $\begingroup$ Would you consider the base problem a bug or just a quirk? $\endgroup$ Sep 14 at 14:29
  • $\begingroup$ Hmmm, tough call. I'm going to go with "quirk". Compiling and imposing strong types on a fluid language like WL is intrinsically difficult (maybe impossible). For example, in WL one can redefine ; but the compiler will make strong assumptions about (apparently) simple expression sequencing. Closer to the case at hand, WL purposefully blurs the distinction between row and column vectors, but a compiler will want to be more strict. So I feel that we will be faced with such compilation quirks for a long time to come. None of this navel-gazing prevents raising a support ticket :) $\endgroup$
    – WReach
    Sep 14 at 22:10
  • $\begingroup$ Here's my contribution for Confuse-A-Compiler Day: $/:HoldPattern[a_;$;b_]:=(b;a);Compile[{},Print[1];$;Print[2]][] :D $\endgroup$
    – WReach
    Sep 14 at 22:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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