POOMA: A C++ Toolkit for High-Performance Parallel Scientific Computing | ||
---|---|---|
Prev | Chapter 5. Array Containers | Next |
In the previous section, we explained how to declare and initialize Arrays. In this section, we explain how to access individual values stored within an Array and how to copy Arrays. In Chapter 7, we explain how to use entire Arrays in data-parallel statements, including how to print them. In Chapter 8, we extend this capability to work on subsets.
In its simplest form, an Array stores individual values, permitting access to these values. For a C++ array, the desired index is specified within square brackets following the array's name. For POOMA Arrays, the desired index is specified within parentheses following the Array's name. The same notation is used to read and write values. For example, the following code prints the initial value at index (2,-2) and increments its value, printing the new value:
Array<2,int,Brick> a(Interval<1>(0,3), Interval<1>(-2,4), ModelElement<int>(4)); std::cout < < a(2,-2) < < std::endl; ++a(2,-2); std::cout < < a(2,-2) < < std::endl;4 and then 5 are printed. An index specification for an Array usually has as many integers as dimensions, all separated by commas, but the Array's engine may permit other notation such as using strings or floating-point numbers.
For read-only access to a value, use the read member function, which takes the same index notation as its nameless read-write counterpart:
std::cout < < a.read(2,-2) < < std::endl;Using read sometimes permits the optimizer to produce faster executing code.
Copying Arrays requires little execution time because Arrays have reference semantics. That is, a copy of an Array and the Array itself share the same underlying data. Changing a value in one changes it in the other. Example 5-1 illustrates this behavior. Initially, all values in the array a are 4. The b array is initialized using a so it shares the same values as a. Thus, changing the former's value also changes the latter's value. Function arguments are also initialized so changing their underlying values also changes the calling function's values. For example, the changeValue function changes the value at index (0,0) for both its function argument and a.
Example 5-1. Copying Arrays
#include "Pooma/Pooma.h" #include "Pooma/Arrays.h" #include <iostream> // Changes the Array value at index (0,0). void changeValue(Array<2,int,Brick>& z) { z(0,0) = 6; } int main(int argc, char *argv[]) { Pooma::initialize(argc,argv); Array<2,int,Brick> a(3,4, ModelElement<int>(4)); std::cout < < "Initial value:\n"; std::cout < < "a: " < < a(0,0) < < std::endl; // Array copies share the same underlying values. // Explicit initialization uses reference semantics // so changing the copy's value at (0,0) also // changes the original's value. Array<2,int,Brick> b(a); b(0,0) = 5; std::cout < < "After explicit initialization.\n"; std::cout < < "a: " < < a(0,0) < < std::endl; std::cout < < "b: " < < b(0,0) < < std::endl; // Initialization of function arguments also uses // reference semantics. std::cout < < "After function call:\n"; changeValue(a); std::cout < < "a: " < < a(0,0) < < std::endl; std::cout < < "b: " < < b(0,0) < < std::endl; Pooma::finalize(); return 0; }
The separation between a higher-level Array and its lower-level Engine storage permits fast copying. An Array's only data member is its engine, which itself has reference semantics that increments a reference-counted pointer to its data. Thus, copying an Array requires creating a new object with one data member and incrementing a pointer's reference count. Destruction is similarly inexpensive.
Array assignment does not have reference semantics. Thus, the assignment a = b ensures that all of a's values are the same as b at the time of assignment only. Subsequent changes to a's values do not change b's values or vice versa. Assignment is more expensive than creating a reference. Creating a reference requires creating a very small object and incrementing a reference-counted pointer. An assignment requires storage for both the left-hand side and right-hand side operands and traversing all of the right-hand side's data.
The Array class has internal type definitions and constants useful for both compile-time and run-time computations. See Table 5-12. These may be accessed using the Array's type and the scope resolution operator (::). The table begins with a list of internal type definitions, e.g., Array<D,T,E>::This_t. A layout maps a domain index to a particular processor and memory used to compute the associated value. The two internal enumerations dimensions and rank both record the Array's dimension.
Table 5-12. Array Internal Type Definitions and Compile-Time Constants
internal type or compile-time constant | meaning |
---|---|
This_t | the Array's type Array<D,T,E>. |
Engine_t | the Array's Engine type Engine<D,T,E>. |
EngineTag_t | the Array's Engine's tag E. |
Element_t | the type T of values stored in the Array. |
ElementRef_t | the type of references to values stored in the Array (usually T&). |
Domain_t | the type of the Array's domain. |
Layout_t | the type of the Array's layout. |
const int dimensions | the number D of dimensions of the Array. |
const int rank | synonym for dimensions. |
The Array class has several member functions easing access to its domain and engine. The first ten functions listed in Table 5-13 ease access to Array domains. The first three functions are synonyms all returning the Array's domain, which has type Array<D,T,E>::Domain_t (abbreviated Domain_t in the table). The next seven functions query the domain. first, last, and length return the first index, last index, and number of indices for the specified dimension. The domain's dimensions are numbered 0, 1, …, Array<D,T,E>::dimensions-1. If these values are needed for all dimensions, use firsts, lasts, and lengths. The returned Loc<D>s have D entries, one for each dimension. size returns the total number of indices in the entire domain. This is the product of all the dimensions' lengths. The layout member function returns the Array's layout, which specifies the mapping of indices to processors and memory. The last two functions return the Array's engine.
Table 5-13. Array Accessors
Array member function | result |
---|---|
Domain_t domain() | returns the Array's domain. |
Domain_t physicalDomain() | returns the Array's domain. |
Domain_t totalDomain() | returns the Array's domain. |
int first(int dim) | returns the first index value for the specified dimension. |
int last(int dim) | returns the last index value for the specified dimension. |
int length(int dim) | returns the number of indices (including endpoints) for the specified dimension. |
Loc<Dim> firsts() | returns the first index values for all the dimensions. |
Loc<Dim> lasts() | returns the last index values for all the specified dimensions. |
Loc<Dim> lengths() | returns the numbers of indices (including endpoints) for all the specified dimensions. |
long size() | returns the total number of indices in the domain. |
Layout_t layout() | returns the Array's layout. |
Engine_t engine() | returns the Array's engine. |
const Engine_t engine() | returns the Array's engine. |
Internal type definitions, e.g., Domain_t, are listed here without the class type prefix Array<D,T,E>::. |
We illustrate using Array member functions in Example 5-2. The program computes the total number of Array's indices, comparing the result with invoking its size method. Since the Array's name is a, a.size() returns its size. The computeArraySize function also computes the Array's size. This templated function uses its three template parameters to accept any Array, regardless of its dimension, value type, or Engine tag. It begins by obtaining the range of indices for all dimensions and their lengths. Only the latter is necessary for the computation, but using the former further illustrates using member functions. The domain's size is the product of the length of each dimension. Since the lengths are stored in the Loc<D> lens, lens[d] is a Loc<1>, for which its first member function extracts the length. The length Array member function is used in the PAssert.
Example 5-2. Using Array Member Functions
#include "Pooma/Pooma.h" #include "Pooma/Arrays.h" #include <iostream> // Print an Array's Size // This program illustrates using the Array member // functions. computeArraySize's computation is // redundant because Array's size() function computes // the same value, but it illustrates using Array // member functions. template <int Dim,typename Type,typename EngineTag>inline long computeArraySize(const Array<Dim,Type,EngineTag>& a) { const Loc<Dim> fs = a.firsts();
const Loc<Dim> ls = a.lasts(); const Loc<Dim> lens = a.lengths(); long size = 1; for (int d = 0; d < Dim; ++d) { size *= lens[d].first();
// Check that lengths() and our computed lengths agree. PAssert((ls[d]-fs[d]+1).first()==a.length(d));
} return size; } int main(int argc, char *argv[]) { Pooma::initialize(argc,argv); Array<3,int,Brick> a(3,4,5, ModelElement<int>(4)); PAssert(computeArraySize(a) == a.size());
std::cout < < "The array's size is " < < a.size() < < ".\n"; Pooma::finalize(); return 0; }