concurrency::array_view – data source lifetime

The previous posts in this series on C++ AMP array_view covered:

  1. Introduction to array_view and some of its key semantic aspects
  2. Implicit synchronization on destruction of array_views
  3. array_view discard_data function
  4. Caching and coherence policies underlying array_view implementation
  5. Using a staging array as an array_view’s data source

This post will talk about the lifetime management of an array_view’s data source.

array_view data source lifetime

An array_view is bound to a data source and the data source’s memory allocation must outlive the array_view. Any attempts to access an array_view after the data source memory has been de-allocated will result in undefined behavior. Remember to account for the implicit synchronization on destruction of the last array_view of a data source.

Guideline A: Ensure that the memory allocation corresponding to a data source outlives all array_views referencing that data source.

 array_view<float> GenerateRandomNumbers(float *seeds, int count)
 {
     array_view<const float> seedView(count, seeds);
  
     std::vector<float> outRandVec(count);
     array_view<float> outRandView(outRandVec.size(), outRandVec);
     outRandView.discard_data();
  
     parallel_for_each(outRandView.extent, [seedView, outRandView](index<1> idx) restrict(amp) {
         ...
     });
  
     // Guideline A violation: Returning an array_view that uses a local std::vector as its data
     // source which will be destructed at the end of this function. Accessing the array_view after the
     // std::vector data source is destructed has undefined behavior.
     return outRandView;
 }

 

An exception in this regard is the use of a concurrency::array container as the data source of an array_view. The memory allocation underlying a concurrency::array is reference counted and lives as long as there is a live array or array_view reference to it. Hence if you are using a concurrency::array as a data source for an array_view, you need not worry about having to keep the source array object live – it is fine for any array_views created from an array to outlive the array itself. In fact, this is a useful technique if an array_view needs to be encapsulated within another user-defined type. In such a scenario, it is often desirable that the type encapsulating the array_view also encapsulates the data source of the array_view so that their lifetimes are managed together. However, if an object of this user-defined type has to be captured in a parallel_for_each kernel, the data source itself cannot be a data member of this user-defined type (since a concurrency::array, a CPU pointer or an STL container cannot be captured by value in a parallel_for_each kernel per the restrict(amp) restrictions). Creating an array_view over a temporary array solves this problem – the buffer allocation underlying the array/array_view being reference counted, lives even after the temporary array is destructed until the array_view itself is destructed.

 template <typename value_type>
 class Matrix
 {
 public:
     // The Matrix type's array_view data member can be constructed from 
     // a temporary array without the array itself being a data member of the type
     // Memory underlying the array is freed when the array_view member is 
     // destructed in the Matrix destructor
     Matrix(int height, int width)
     : _M_data_view(array<value_type, 2>(height, width))
     {
     }
  
     array_view<value_type> GetRow(int rowNumber) restrict(cpu, amp)
     {
         return _M_data_view[rowNumber];
     }
  
     value_type& operator(int i, int j) restrict(cpu, amp) 
     {
         return _M_data_view(i, j);
     }
  
 private:
     array_view<value_type, 2> _M_data_view;
 };
  
 Matrix<float> mA(M, W);
  
 // The Matrix object can be captured in the parallel_for_each by value
 // as it only contains an array_view data member. If the type has a concurrency::array
 // data member, it would have been illegal to capture a Matrix object by value
 // in the parallel_for_each kernel
 parallel_for_each(mA.extent, [=](index<2> idx) restrict(amp) {
     mA(idx) = fast_math::sqrt(mA(idx));
 });

 

In closing

In this post we looked at the importance of ensuring the right lifetime for an array_view’s data source.

If there are other array_view topics that are not covered on our blog, please let me know so I can address them too. I would love to hear your feedback, comments and questions below or in our MSDN forum.