24
$\begingroup$

When we don't know the number of results that will be generated, the usual way to collect results is Reap/Sow. Another alternative is linked lists. Neither of these are available in compiled functions.

AppendTo does work, but it has an $O(n^2)$ complexity, so it will be unacceptably slow for long result lists.

There was a very clever suggestion to use Internal`Bag in these situations:

Unfortunately a compiled Bag will only hold scalars. Let's try to use vectors:

cf = Compile[{}, 
  Module[{bag = Internal`Bag[{{0, 0, 0}}]}, 
   Do[Internal`StuffBag[bag, {i, i, i}], {i, {1, 2, 3}}]; 
   Internal`BagPart[bag, All]
  ]]

CompilePrint shows that this calls MainEvaluate, so this does not work.

What is the best way to collect a large number of results in a compiled function when the number of results is not known before the computation and the result type is not a (fixed size) vector or matrix?


Benchmarking the answers

  • Andy's answer

    cf = Compile[{len},
           Module[{bag = Internal`Bag[Most[{0}]]}, 
             Do[Internal`StuffBag[bag, {i, i, i}, 1], {i, len}];
             Partition[Internal`BagPart[bag, All], 3]
           ]
         ];
    
    Do[cf[500000], {100}]; // Timing
    
    (* ==> {2.87, Null} *)
    

    I needed to initialize the Bag using bag = Internal`Bag[Most[{0}]] to let Compile know that it is holding integers, not reals (see here).

  • Leonid's answer

    cf2 = 
      Compile[{len},
       Module[{arr, lim, ctr},
        arr = ConstantArray[{0, 0, 0}, 10];
        lim = Length[arr];
        ctr = 1;
        Do[
         If[ctr == lim,
          arr = Join[arr, Table[{0, 0, 0}, {lim}]];
          lim = Length[arr]];
         arr[[ctr++]] = {i, i, i},
    
         {i, len}
        ];
        Take[arr, ctr - 1]
       ]
      ];
    
    Do[cf2[500000], {100}]; // Timing
    
    (* ==> {16.474, Null} *)
    

Comparing the computational complexity of the two solutions by direct measurement:

data = Table[
   {Round[2^k], First@AbsoluteTiming@Do[cf[Round[2^k]], {100}]}, 
   {k, 13, 19, 1/2}];
data2 = Table[
   {Round[2^k], First@AbsoluteTiming@Do[cf2[Round[2^k]], {100}]}, 
   {k, 13, 19, 1/2}];

ListLogLogPlot[{data, data2}]

Mathematica graphics

(They're the same.)

$\endgroup$
3
  • $\begingroup$ Do you mean when the number of results is unknown and the type of each result is a fixed dimensional list? Or you're asking for the more general case of different lengths and dimensions of each result to be collected? $\endgroup$
    – Rojo
    Commented Feb 17, 2012 at 20:17
  • $\begingroup$ @Rojo I am asking for the case when each result is the same type (a fixed dimensional tensor) but the number of results is not known beforehand. $\endgroup$
    – Szabolcs
    Commented Feb 17, 2012 at 21:02
  • 1
    $\begingroup$ I think the reason cf2 performs poorly is that it is not completely compiled. There is still a call to MainEvaluate. $\endgroup$
    – RunnyKine
    Commented Jun 19, 2015 at 23:54

2 Answers 2

17
$\begingroup$

Assuming you know the dimensions of the pieces that you want to come out you can always add a second argument to Internal`StuffBag that indicates the rank of the elements going in. The result is still flat so you have to partition after the fact.

cf = Compile[{}, Module[{bag = Internal`Bag[]},
    Do[Internal`StuffBag[bag, {i, i, i}, 1], {i, {0, 1, 2, 3}}];
    Partition[Internal`BagPart[bag, All], 3]]];

Here I'm indicating that vectors will be going in to the bag. I would specify 2 for a matrix. Notice that it no longer calls MainEvaluate.

In[25]:= StringFreeQ[CompilePrint[cf], "MainEvaluate"]

Out[25]= True

In[20]:= cf[]

Out[20]= {{0., 0., 0.}, {1., 1., 1.}, {2., 2., 2.}, {3., 3., 3.}}
$\endgroup$
7
  • 1
    $\begingroup$ very useful information, +1. Cheating a bit though :) $\endgroup$
    – acl
    Commented Feb 18, 2012 at 0:07
  • 3
    $\begingroup$ Notice that this returns reals rather than integers. What is the best way to get it to work with integers (and also run faster)? @halirutan's trick of initializing the Bag as bag = Internal`Bag[Most[{0}]] does work, but I find it both ugly and confusing, so I was wondering if there is a better way (maybe through Compile's 3rd argument). $\endgroup$
    – Szabolcs
    Commented Feb 18, 2012 at 8:10
  • 1
    $\begingroup$ @Szabolcs there may well be another way but I'm unaware of it. I typically initialize the bag as you have if I want to avoid coercion to reals. $\endgroup$
    – Andy Ross
    Commented Feb 18, 2012 at 17:53
  • $\begingroup$ Hi, @AndyRoss. I still can not understand "indicates the rank of the elements going in". When outside Compile, we can directly stuff 1d list into a bag, and got a 2d list. For example, myBag = Internal`Bag[];Do[Internal`StuffBag[myBag, {i, i, i}], {i, 3}];Internal`BagPart[myBag, All]. Then why is it not working in Compile? In Compile[{}, Module[{myBag = Internal`Bag[]}, Do[Internal`StuffBag[myBag, {i, i, i}], {i, 3}]; Internal`BagPart[myBag, All]]]`, `Internal`StuffBag[myBag, {i, i, i}] can not be compiled. $\endgroup$
    – matheorem
    Commented Sep 1, 2016 at 15:47
  • $\begingroup$ @matheorem By "can not be compiled" I assume you mean it makes a call to MainEvaluate because this compiles just fine for me otherwise. By adding the second argument indicating the dimension (I was sloppy with the work "rank") of the input it tells the compiler to expect a vector and does not require calling MainEvaluate $\endgroup$
    – Andy Ross
    Commented Sep 1, 2016 at 16:19
8
$\begingroup$

As I mentioned in my recent answer, another alternative is to implement a version of a dynamic array inside Compile, as say arr = Table[{0,0},{10}] (collecting vectors of length 2, in this example) . Set up a variable (say lim) which gives the current size limit, initialize it to the initial size of the allocated array, and another one (say ctr) which counts the current maximal used position. Then, you can do something like

If[ctr==lim,
  arr = Join[arr,Table[{0,0},{lim}]];
  lim = Length[arr]
]

or, instead of doubling, use some other array expansion policy (other exponent, or additive, may depend on the problem). This adds one extra instruction (check) in your inner loop, but potentially saves you memory, and, unlike Internal`Bag, can be returned from Compile. This won't work when your results are lists of different lengths (dimensions), however.

$\endgroup$
9
  • $\begingroup$ This is what I would do in C too. $\endgroup$
    – Szabolcs
    Commented Feb 17, 2012 at 21:10
  • $\begingroup$ Can you please take a quick look at my update and verify that I did the benchmarking correctly? (Just to make sure the difference is not because of something I overlooked.) $\endgroup$
    – Szabolcs
    Commented Feb 18, 2012 at 8:31
  • 1
    $\begingroup$ Leonid, congratulations on 6K rep. :-) $\endgroup$
    – Mr.Wizard
    Commented Feb 19, 2012 at 4:45
  • 1
    $\begingroup$ @Mr.Wizard No, this is correct. The rep measures the level of participation on the site, as well as the quality of the answers. There are people who are many times more knowledgable than me for example, but can'y afford to participate a lot, and have a lower rep. I don't think we should pay much attention to the rep - what matters is that the questions and answers are useful to others. You bring a lot to this site, so your rep is more than well-deserved. $\endgroup$ Commented Feb 19, 2012 at 13:59
  • 1
    $\begingroup$ I really appreciate that. I still would like to meet one of these "many times more knowledgeable" people you speak of. Or perhaps not. $\endgroup$
    – Mr.Wizard
    Commented Feb 19, 2012 at 14:09

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.