Array

The array is a simple concept over the data in oneDAL. It represents a storage that:

  1. Holds the data allocated inside it or references to the external data. The data are organized as one homogeneous and contiguous memory block.

  2. Contains information about the memory block’s size.

  3. Represents either immutable or mutable data.

  4. Provides an ability to change the data state from immutable to mutable one.

  5. Holds ownership information on the data (see the data ownership requirements section).

  6. Ownership information on the data can be shared between several arrays. It is possible to create a new array from another one without any data copies.

Usage example

The following listing provides a brief introduction to the array API and an example of basic usage scenario:

#include <CL/sycl.hpp>
#include <iostream>
#include <string>
#include "oneapi/dal/array.hpp"

using namespace oneapi;

void print_property(const std::string& description, const auto& property) {
   std::cout << description << ": " << property << std::endl;
}

int main() {
   sycl::queue queue { sycl::default_selector() };

   constexpr std::int64_t data_count = 4;
   const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f };

   // Creating an array from immutable user-defined memory
   auto arr_data = dal::array<float>::wrap(data, data_count);

   // Creating an array from internally allocated memory filled by ones
   auto arr_ones = dal::array<float>::full(queue, data_count, 1.0f);

   print_property("Is arr_data mutable", arr_data.has_mutable_data()); // false
   print_property("Is arr_ones mutable", arr_ones.has_mutable_data()); // true

   // Creating new array from arr_data without data copy - they share ownership information.
   dal::array<float> arr_mdata = arr_data;

   print_property("arr_mdata elements count", arr_mdata.get_count()); // equal to data_count
   print_property("Is arr_mdata mutable", arr_mdata.has_mutable_data()); // false

   /// Copying data inside arr_mdata to new mutable memory block.
   /// arr_data still refers to the original data pointer.
   arr_mdata.need_mutable_data(queue);

   print_property("Is arr_data mutable", arr_data.has_mutable_data()); // false
   print_property("Is arr_mdata mutable", arr_mdata.has_mutable_data()); // true

   queue.submit([&](sycl::handler& cgh){
      auto mdata = arr_mdata.get_mutable_data();
      auto cones = arr_ones.get_data();
      cgh.parallel_for<class array_addition>(sycl::range<1>(data_count), [=](sycl::id<1> idx) {
         mdata[idx[0]] += cones[idx[0]];
      });
   }).wait();

   std::cout << "arr_mdata values: ";
   for(std::int64_t i = 0; i < arr_mdata.get_count(); i++) {
      std::cout << arr_mdata[i] << ", ";
   }
   std::cout << std::endl;

   return 0;
}

Data ownership requirements

The array shall satisfy the following requirements on managing the memory blocks:

  1. An array shall retain:

    • A pointer to the immutable data block of size count;

    • A pointer to the mutable data block of size count.

  2. If an array represents mutable data, both pointers shall point to the mutable data block.

  3. If an array represents immutable data, pointer to the mutable data block shall be nullptr.

  4. An array shall use shared ownership semantics to manage the lifetime of the stored data block:

    • Several array objects may own the same data block;

    • An array releases the ownership when one of the following happens:

      • The array owning the data block is destroyed;

      • The array owning the data block is assigned another memory block via operator= or reset();

    • If the array that releases the ownership is the last remaining object owning the data block, the release of ownership is followed by the data block deallocation.

    • The data block is deallocated using the deleter object that is provided to array during construction. If no deleter object provided, an array calls the default deallocating function that corresponds to the internal memory allocation mechanism.

  5. If a managed pointer to the data block is replaced by another pointer via reset(), the array that managed the pointer releases the ownership of it and starts managing the lifetime of the data block represented by the other pointer.

  6. If an array changes its state from immutable to mutable via need_mutable_data(), it releases the ownership of immutable data block and start managing lifetime of the mutable data block.

  7. An array object may own no data. An array like this is called zero-sized:

    • Pointers to the immutable and mutable data of the zero-sized array shall be nullptr;

    • The data block size count shall be 0.

Implementation notes

A typical array implementation may be organized in the following way:

  1. An array class has the following member variables:

    • A pointer to the immutable data block;

    • A pointer to the mutable data block;

    • A pointer to the ownership structure that implements the shared ownership semantics;

    • The data block size count;

  2. An ownership structure is an object that stores:

    • A pointer to either immutable or mutable data block;

    • The deleter object;

    • The reference count (the number of array objects that own the associated data block);

  3. If an array starts managing the lifetime of the data block represented by the pointer p and deleter d, it creates the ownership structure object and initialize it with p and d. The reference count of the ownership structure is assigned one.

  4. If an array object releases the ownership, the reference count of the ownership structure is decremented.

    • If that count reaches zero, the ownership structure deallocates the memory block and the array destroys the ownership structure.

    • If that count is greater than zero, the ownership structure is not destroyed.

  5. If a copy of the array object is created, the reference count of the ownership structure is incremented and a pointer to the same ownership structure is assigned to the created copy. The other member variables of an array class are copied as is.

Note

You may choose an arbitrary implementation strategy that satisfies array requirements.

Programming interface

All types and functions in this section shall be declared in the oneapi::dal namespace and be available via inclusion of the oneapi/dal/array.hpp header file.

All the array class methods can be divided into several groups:

  1. Constructors that are used to create an array from external, mutable or immutable memory.

  2. Constructors and assignment operators that are used to create an array that shares its data with another one.

  3. The group of reset() methods that are used to re-assign an array to another external memory block.

  4. The group of reset() methods that are used to re-assign an array to an internally allocated memory block.

  5. The methods that are used to access the data.

  6. Static methods that provide simplified ways to create an array either from external memory or by allocating it within a new object.

template <typename Data>
class array {
public:
    using data_t = Data;

    static array<Data> empty(const sycl::queue& queue,
                             std::int64_t count,
                             const sycl::usm::alloc& alloc = sycl::usm::alloc::shared);

    template <typename Element>
    static array<Data> full(sycl::queue& queue,
                            std::int64_t count,
                            Element&& element,
                            const sycl::usm::alloc& alloc = sycl::usm::alloc::shared);

    static array<Data> zeros(sycl::queue& queue,
                             std::int64_t count,
                             const sycl::usm::alloc& alloc = sycl::usm::alloc::shared);

    template <typename ExtData>
    static array<Data> wrap(ExtData* data,
                            std::int64_t count,
                            const std::vector<sycl::event>& dependencies = {});

    array();

    array(const array<Data>& other);

    array(array<Data>&& other);

    template <typename ExtData, typename Deleter>
    explicit array(const sycl::queue& queue,
                   ExtData* data,
                   std::int64_t count,
                   Deleter&& deleter,
                   const std::vector<sycl::event>& dependencies = {});

    template <typename RefData, typename ExtData>
    explicit array(const array<RefData>& ref, ExtData* data, std::int64_t count);

    array<Data> operator=(const array<Data>& other);

    array<Data> operator=(array<Data>&& other);

    const Data* get_data() const noexcept;

    bool has_mutable_data() const noexcept;

    Data* get_mutable_data() const;

    array& need_mutable_data(sycl::queue& queue,
                             const sycl::usm::alloc& alloc = sycl::usm::alloc::shared);

    std::int64_t get_count() const noexcept;

    std::int64_t get_size() const noexcept;

    const Data& operator[](std::int64_t index) const noexcept;

    void reset();

    void reset(const sycl::queue& queue,
               std::int64_t count,
               const sycl::usm::alloc& alloc = sycl::usm::alloc::shared);

    template <typename ExtData, typename Deleter>
    void reset(ExtData* data,
               std::int64_t count,
               Deleter&& deleter,
               const std::vector<sycl::event>& dependencies = {});

    template <typename RefData, typename ExtData>
    void reset(const array<RefData>& ref, ExtData* data, std::int64_t count);
};
template<typename Data>
class array
Template Parameters

Data – The type of the memory block elements within the array. Data can represent any data type.

Public Static Methods

static array<Data> empty(const sycl::queue &queue, std::int64_t count, const sycl::usm::alloc &alloc = sycl::usm::alloc::shared)

Creates a new array instance by allocating a mutable memory block. The created array manages the lifetime of the allocated memory block. The function is not required to initialize the values of the allocated memory block.

Parameters
  • queue – The SYCL* queue object.

  • count – The number of elements of type Data to allocate memory for.

  • alloc – The kind of USM to be allocated.

Preconditions
count > 0
Postconditions
get_count() == count
has_mutable_data() == true
template<typename Element>
static array<Data> full(sycl::queue &queue, std::int64_t count, Element &&element, const sycl::usm::alloc &alloc = sycl::usm::alloc::shared)

Creates a new array instance by allocating a mutable memory block and filling its content with a scalar value. The created array manages the lifetime of the allocated memory block.

Template Parameters

Element – The type from which array elements of type Data can be constructed.

Parameters
  • queue – The SYCL* queue object.

  • count – The number of elements of type Data to allocate memory for.

  • element – The value that is used to fill a memory block.

  • alloc – The kind of USM to be allocated.

Preconditions
count > 0
Postconditions
get_count() == count
has_mutable_data() == true
get_data()[i] == element, 0 <= i < count
static array<Data> zeros(sycl::queue &queue, std::int64_t count, const sycl::usm::alloc &alloc = sycl::usm::alloc::shared)

Creates a new array instance by allocating a mutable memory block and filling its content with zeros. The created array manages the lifetime of the allocated memory block.

Parameters
  • queue – The SYCL* queue object.

  • count – The number of elements of type Data to allocate memory for.

  • alloc – The kind of USM to be allocated.

Preconditions
count > 0
Postconditions
get_count() == count
has_mutable_data() == true
get_data()[i] == 0, 0 <= i < count
template<typename ExtData>
static array<Data> wrap(ExtData *data, std::int64_t count, const std::vector<sycl::event> &dependencies = {})

Creates a new array instance from a pointer to externally-allocated memory block. The created array does not manage the lifetime of the user-provided memory block. It is the responsibility of the programmer to make sure that data pointer remains valid as long as this array object exists.

Template Parameters

ExtData – Either Data or const Data type.

Parameters
  • data – The pointer to the mutable or immutable externally-allocated memory block.

  • count – The number of elements of type Data in the memory block.

  • dependencies – Events indicating the availability of the data for reading or writing.

Preconditions
data != nullptr
count > 0
Postconditions
get_count() == count
get_data() == data
has_mutable_data() == false

Constructors

array()

Creates a zero-sized array without memory allocation.

Postconditions
get_count() == 0
get_data() == nullptr
has_mutable_data() == false
array(const array<Data> &other)

Creates a new array instance that shares an ownership with the other array.

array(array<Data> &&other)

Creates a new array instance that transfers the ownership from the other array. After the construction of a new instance, the behaviour of the other is defined by the implementation.

Postconditions
other.get_count() == 0
other.get_data() == nullptr
has_mutable_data() == false
template<typename ExtData, typename Deleter>
array(const sycl::queue &queue, ExtData *data, std::int64_t count, Deleter &&deleter, const std::vector<sycl::event> &dependencies = {})

Creates a new array instance from a pointer to externally-allocated memory block. The created array manages the lifetime of the user-provided memory block. The memory block is deallocated using a custom deleter object provided by the user.

Template Parameters
  • ExtData – Either Data or const Data type.

  • Deleter – The type of a deleter used to deallocate the data. The expression deleter(data) must be well-formed (can be compiled) and not throw any exceptions.

Parameters
  • queue – The SYCL* queue object.

  • data – The pointer to the mutable or immutable externally-allocated mutable data.

  • count – The number of elements of type Data in the memory block.

  • deleter – The object used to deallocate data.

  • dependencies – Events that indicate when data becomes ready to be read or written.

Preconditions
data != nullptr
count > 0
Postconditions
get_count() == count
get_data() == data
has_mutable_data() == true
get_mutable_data() == data
template<typename RefData, typename ExtData>
array(const array<RefData> &ref, ExtData *data, std::int64_t count)

Creates a new array instance that shares the ownership with the reference array while storing the pointer to another memory block provided by the user. The lifetime of the user-provided memory block is not managed by the created array. One of the use cases of this constructor is the creation of an array with an offset, for example, array{ other, other.get_data() + offset }. The array created this way shares the ownership with the other, but points to its data with an offset. It is the responsibility of the programmer to make sure that data pointer remains valid as long as this array object exists.

Template Parameters
  • RefData – The type of elements in the reference array.

  • ExtData – Either Data or const Data type.

Parameters
  • ref – The reference array which shares the ownership with the created one.

  • data – The unmanaged pointer to the mutable or immutable externally-allocated memory block.

  • count – The number of elements of type Data in the data.

Preconditions
data != nullptr
count > 0
Postconditions
get_count() == count
get_data() == data

Public Methods

array<Data> operator=(const array<Data> &other)

Replaces the immutable and mutable data pointers and the number of elements by the values stored in the other array.

Postconditions
get_data() == other.get_data()
get_count() == other.get_count()
get_mutable_data() == other.get_mutable_data()
array<Data> operator=(array<Data> &&other)

Replaces the immutable and mutable data pointers and the number of elements by the values stored in the other array.

Postconditions
get_data() == other.get_data()
get_count() == other.get_count()
get_mutable_data() == other.get_mutable_data()
const Data *get_data() const noexcept

The pointer to the immutable memory block.

bool has_mutable_data() const noexcept

Returns whether an array contains mutable data or not.

Data *get_mutable_data() const

The pointer to the mutable memory block.

Preconditions
has_mutable_data() == true, othewise throws domain_error
array &need_mutable_data(sycl::queue &queue, const sycl::usm::alloc &alloc = sycl::usm::alloc::shared)

Does nothing if an array contains mutable data. Otherwise, allocates a mutable memory block and copies the content of the immutable memory block into it. The array manages the lifetime of the allocated mutable memory block. Returns the reference to the same array instance.

Parameters
  • queue – The SYCL* queue object.

  • alloc – The kind of USM to be allocated.

Postconditions
has_mutable_data() == true
std::int64_t get_count() const noexcept

The number of elements of type Data in a memory block.

std::int64_t get_size() const noexcept

The size of memory block in bytes.

const Data &operator[](std::int64_t index) const noexcept

Provides a read-only access to the elements of an array. No bounds checking is performed.

void reset()

Releases the ownership of the managed memory block.

Preconditions
count > 0
Postconditions
get_count() == count
has_mutable_data() == true
void reset(const sycl::queue &queue, std::int64_t count, const sycl::usm::alloc &alloc = sycl::usm::alloc::shared)

Releases the ownership of the managed memory block and replaces it by a newly allocated mutable memory block. The lifetime of the allocated memory block is managed by the array.

Parameters
  • queue – The SYCL* queue object.

  • count – The number of elements of type Data to allocate memory for.

  • alloc – The kind of USM to be allocated.

Preconditions
count > 0
Postconditions
get_count() == count
template<typename ExtData, typename Deleter>
void reset(ExtData *data, std::int64_t count, Deleter &&deleter, const std::vector<sycl::event> &dependencies = {})

Releases the ownership of the managed memory block and replace it by a pointer to externally-allocated memory block. The lifetime of the memory block is managed by the array. The memory block is deallocated using a custom deleter object provided by the user.

Template Parameters
  • ExtData – Either Data or const Data type.

  • Deleter – The type of a deleter used to deallocate the data. The expression deleter(data) must be well-formed (can be compiled) and not throw any exceptions.

Parameters
  • data – The pointer to the to the mutable or immutable externally-allocated memory block.

  • count – The number of elements of type Data in the data.

  • deleter – The object used to deallocate data.

  • dependencies – Events indicating the availability of the data for reading or writing.

Preconditions
data != nullptr
count > 0
Postconditions
get_count() == count
get_data() == data
has_mutable_data() == true
get_mutable_data() == data
template<typename RefData, typename ExtData>
void reset(const array<RefData> &ref, ExtData *data, std::int64_t count)

Releases the ownership of the managed memory block and starts managing the lifetime of the reference array while storing the pointer to another memory block provided by the user. The lifetime of the user-provided memory block is not managed. It is the responsibility of the programmer to make sure that data pointer remains valid as long as this array object exists.

Template Parameters
  • RefData – The type of elements in the reference array.

  • ExtData – Either Data or const Data type.

Parameters
  • ref – The reference array which shares the ownership with the created one.

  • data – The unmanaged pointer to the mutable or immutable externally-allocated memory block.

  • count – The number of elements of type Data in the data.

Preconditions
data != nullptr
count > 0
Postconditions
get_count() == count
get_data() == data