Core containers

Warning

Axom’s containers are currently the target of a refactoring effort. The following information is subject to change, as are the interfaces for the containers themselves. When changes are made to the Axom containers, the changes will be reflected here.

Axom Core contains the Array, ArrayView, and StackArray classes. Among other things, these data containers facilitate porting code that uses std::vector to GPUs.

Array is a multidimensional contiguous container template. In the 1-dimensional case, this class behaves similar to std::vector. In higher dimensions, some vector-like functionality, such as push_back, are not available and multidimensional-specific operations will mirror the ndarray provided by numpy when possible.

The Array object manages all memory. Typically, the Array object will allocate extra space to facilitate the insertion of new elements and minimize the number of reallocations. The actual capacity of the array (i.e., total number of elements that the Array can hold) can be queried via the capacity() method. When allocated memory is used up, inserting a new element triggers a reallocation. At each reallocation, extra space is allocated according to the resize_ratio parameter, which is set to 2.0 by default. To return all extra memory, an application can call shrink().

Warning

Reallocations tend to be costly operations in terms of performance. Use reserve() when the number of nodes is known a priori, or use a constructor that takes an actual size and capacity when possible.

Note

The Array destructor deallocates and returns all memory associated with it to the system.

Here’s an example showing how to use Array instead of std::vector.

  // Here is an Array of ints with length three.
  axom::Array<int> a(3);
  std::cout << "Length of a = " << a.size() << std::endl;
  a[0] = 2;
  a[1] = 5;
  a[2] = 11;

  // An Array increases in size if a value is pushed back.
  a.push_back(4);
  std::cout << "After pushing back a value, a's length = " << a.size()
            << std::endl;

  // You can also insert a value in the middle of the Array.
  // Here we insert value 6 at position 2 and value 1 at position 4.
  showArray(a, "a");
  a.insert(2, 6);
  a.insert(4, 1);
  std::cout << "After inserting two values, ";
  showArray(a, "a");

The output of this example is:

Length of a = 3
After appending a value, a's length = 4
Array a = [2, 5, 11, 4]
After inserting two values, Array a = [2, 5, 6, 11, 1, 4]

Applications commonly store tuples of data in a flat array or a std::vector. In this sense, it can be thought of as a two-dimensional array. Array supports arbitrary dimensionalities but an alias, MCArray (or Multi-Component Array), is provided for this two-dimensional case.

The MCArray (i.e., Array<T, 2>) class formalizes tuple storage, as shown in the next example.

  // Here is an MCArray of ints, containing two triples.
  const int numTuples = 2;
  const int numComponents = 3;
  axom::MCArray<int> b(numTuples, numComponents);
  // Set tuple 0 to (1, 4, 2).
  b(0, 0) = 1;
  b(0, 1) = 4;
  b(0, 2) = 2;
  // Set tuple 1 to one tuple, (8, 0, -1).
  // The first argument to set() is the buffer to copy into the MCArray, the
  // second is the number of tuples in the buffer, and the third argument
  // is the first tuple to fill from the buffer.
  int ival[3] = {8, 0, -1};
  b.set(ival, 3, 3);

  showTupleArray(b, "b");

  // Now, insert two tuples, (0, -1, 1), (1, -1, 0), into the MCArray, directly
  // after tuple 0.
  int jval[6] = {0, -1, 1, 1, -1, 0};
  b.insert(1, numTuples * numComponents, jval);

  showTupleArray(b, "b");

The output of this example is:

MCArray b with 2 3-tuples = [
  [1, 4, 2]
  [8, 0, -1]
]
MCArray b with 4 3-tuples = [
  [1, 4, 2]
  [0, -1, 1]
  [1, -1, 0]
  [8, 0, -1]
]

It is also often useful to wrap an external, user-supplied buffer without taking ownership of the data. For this purpose Axom provides the ArrayView class, which is a lightweight wrapper over a buffer that provides one- or multi-dimensional indexing/reshaping semantics. For example, it might be useful to reinterpret a flat (one-dimensional) array as a two-dimensional array. This is accomplished via MCArrayView which, similar to the MCArray alias, is an alias for ArrayView<T, 2>.

  // The internal buffer maintained by an MCArray is accessible.
  int* pa = a.data();
  // An MCArray can be constructed with a pointer to an external buffer.
  // Here's an Array interpreting the memory pointed to by pa as three 2-tuples.
  axom::MCArrayView<int> c(pa, 3, 2);

  showArray(a, "a");
  showTupleArrayView(c, "c");

  // Since c is an alias to a's internal memory, changes affect both Arrays.
  a[0] = 1;
  c(1, 1) = 9;

  std::cout
    << "Array a and MCArrayView c use the same memory, a's internal buffer."
    << std::endl;
  showArray(a, "a");
  showTupleArrayView(c, "c");

The output of this example is:

Array a = [2, 5, 6, 11, 1, 4]
MCArrayView c with 3 2-tuples = [
  [2, 5]
  [6, 11]
  [1, 4]
]
Array a and MCArrayView c use the same memory, a's internal buffer.
Array a = [1, 5, 6, 9, 1, 4]
MCArrayView c with 3 2-tuples = [
  [1, 5]
  [6, 9]
  [1, 4]
]

Note

The set of permissible operations on an ArrayView is somewhat limited, as operations that would cause the buffer to resize are not permitted.

In the future, it will also be possible to restride an ArrayView.

Iteration is also identical between the Array and ArrayView classes. In particular:

  • operator() indexes into multidimensional data. Currently, the number of indexes passed must match the dimensionality of the array.
  • operator[] indexes into the full buffer, i.e., arr[i] is equivalent to arr.data()[i].
  • begin() and end() refer to the full buffer.

Consider the following example:

  // Iteration over multidimensional arrays uses the shape() method
  // to retrieve the extents in each dimension.
  for(int i = 0; i < c.shape()[0]; i++)
  {
    for(int j = 0; j < c.shape()[1]; j++)
    {
      // Note that c's operator() accepts two arguments because it is two-dimensional
      std::cout << "In ArrayView c, index (" << i << ", " << j << ") yields "
                << c(i, j) << std::endl;
    }
  }

  // To iterate over the "flat" data in an Array, regardless of dimension,
  // use a range-based for loop.
  std::cout << "Range-based for loop over ArrayView c yields: ";
  for(const int value : c)
  {
    std::cout << value << " ";
  }
  std::cout << std::endl;

  // Alternatively, the "flat" data can be iterated over with operator[]
  // from 0 -> size().
  std::cout << "Standard for loop over ArrayView c yields: ";
  for(int i = 0; i < c.size(); i++)
  {
    std::cout << c[i] << " ";
  }
  std::cout << std::endl;

The output of this example is:

In ArrayView c, index (0, 0) yields 1
In ArrayView c, index (0, 1) yields 5
In ArrayView c, index (1, 0) yields 6
In ArrayView c, index (1, 1) yields 9
In ArrayView c, index (2, 0) yields 1
In ArrayView c, index (2, 1) yields 4
Range-based for loop over ArrayView c yields: 1 5 6 9 1 4
Standard for loop over ArrayView c yields: 1 5 6 9 1 4

The StackArray class is a work-around for a limitation in older versions of the nvcc compiler, which do not capture arrays on the stack in device lambdas. More details are in the API documentation and in the tests.