From c5646ac22496b594ee15f62d9dbc77b6ecadf1da Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Mon, 26 Aug 2024 17:02:39 -0600 Subject: [PATCH] Transfer portable array from Singe to Ports-of-Call. --- doc/sphinx/src/using.rst | 10 ++ ports-of-call/array.hpp | 270 +++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 2 +- test/test_array.cpp | 278 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 ports-of-call/array.hpp create mode 100644 test/test_array.cpp diff --git a/doc/sphinx/src/using.rst b/doc/sphinx/src/using.rst index 909611ea..40a9d7a5 100644 --- a/doc/sphinx/src/using.rst +++ b/doc/sphinx/src/using.rst @@ -220,3 +220,13 @@ data pointer. It accepts anywhere between 1 and 6 sizes. ``PortableMDArray`` also supports some simple boolean comparitors, such as ``==`` and arithmetic such as ``+``, and ``-``. + +array.hpp +^^^^^^^^^ + +``PortsOfCall::array`` is intended to be a drop-in replacement for ``std::array``, with the +exception that it works on GPUs. As of C++17, ``std::array::fill`` and ``std::array::swap`` are +not yet ``constexpr``, so even with the "relaxed ``constexpr``" compilation mode ``std::array`` is +not feature-complete on GPUs. This will change when those member functions become ``constexpr`` in +C++20. + diff --git a/ports-of-call/array.hpp b/ports-of-call/array.hpp new file mode 100644 index 00000000..8eb90cc0 --- /dev/null +++ b/ports-of-call/array.hpp @@ -0,0 +1,270 @@ +#ifndef _PORTS_OF_CALL_ARRAY_HPP_ +#define _PORTS_OF_CALL_ARRAY_HPP_ + +#include "portability.hpp" +#include "portable_errors.hpp" + +#include +#include +#include + +namespace PortsOfCall { + +//! The array class is a constexpr (mostly) replacement for std::array +/*! +std::array is not fully constexpr until C++20. Currently we are relying on +"constexpr"-ness to offload to GPUs. PortsOfCall::array provides a +constexpr replacement for std::array with the following notable exceptions: + - the reverse iterators are not provided because std::reverse_iterator is +not constexpr until C++17 (could be provided with modest effort) + + - the `at` function is not provided because it bounds checks and throws an +exception which we cannot do on a GPU (it could be added and made +non-constexpr) + + - no std::get (cannot be provided without undefined behavior) +*/ + +namespace array_detail { + +template +struct bits { + using type = T[N]; + + template + PORTABLE_FORCEINLINE_FUNCTION static constexpr T &ref_access(type &array, IntT index) { + return array[index]; + } + + template + PORTABLE_FORCEINLINE_FUNCTION static constexpr T const &ref_access(type const &array, + IntT index) { + return array[index]; + } + + PORTABLE_FORCEINLINE_FUNCTION static constexpr T *ptr_access(type &array) { + return static_cast(array); + } + + PORTABLE_FORCEINLINE_FUNCTION static constexpr T const *ptr_access(type const &array) { + return static_cast(array); + } +}; + +template +struct bits { + using type = T[1]; + + template + PORTABLE_FORCEINLINE_FUNCTION static constexpr T &ref_access(type &arr, IntT) { + return arr[0]; + } + + template + PORTABLE_FORCEINLINE_FUNCTION static constexpr T const &ref_access(type const &arr, + IntT) { + return arr[0]; + } + + PORTABLE_FORCEINLINE_FUNCTION static constexpr T *ptr_access(type &arr) { + return (T *)&arr; + } + + PORTABLE_FORCEINLINE_FUNCTION static constexpr T const *ptr_access(type const &arr) { + return (T const *)&arr; + } +}; +} // namespace array_detail + +template +struct array { + // member types + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = T &; + using const_reference = T const &; + using pointer = value_type *; + using const_pointer = value_type const *; + using iterator = pointer; + using const_iterator = const_pointer; + + //! The array being wrapped by this class + typename array_detail::bits::type arr{}; + + //! Index operator that returns a reference to the desired element. + PORTABLE_FUNCTION constexpr reference operator[](size_type index) { + return array_detail::bits::ref_access(arr, index); + } + + //! bounds-checked equivalent to index operator + PORTABLE_FUNCTION constexpr reference at(size_type index) { + if (index >= size()) { + PORTABLE_ALWAYS_THROW_OR_ABORT("invalid index."); + } + return array_detail::bits::ref_access(arr, index); + } + + //! Index operator that returns a const reference to the desired element. + PORTABLE_FUNCTION constexpr const_reference operator[](size_type index) const { + return array_detail::bits::ref_access(arr, index); + } + + //! bounds-checked equivalent to index operator + PORTABLE_FUNCTION constexpr const_reference at(size_type index) const { + if (index >= size()) { + PORTABLE_ALWAYS_THROW_OR_ABORT("invalid index."); + } + return array_detail::bits::ref_access(arr, index); + } + + //! access first element of array + PORTABLE_FUNCTION constexpr reference front() { + return array_detail::bits::ref_access(arr, 0); + } + + //! access first element of const array + PORTABLE_FUNCTION constexpr const_reference front() const { + return array_detail::bits::ref_access(arr, 0); + } + + //! access last element of array + PORTABLE_FUNCTION constexpr reference back() { + return N ? array_detail::bits::ref_access(arr, N - 1) + : array_detail::bits::ref_access(arr, 0); + } + + //! access first element of const array + PORTABLE_FUNCTION constexpr const_reference back() const { + return N ? array_detail::bits::ref_access(arr, N - 1) + : array_detail::bits::ref_access(arr, 0); + } + + //! get pointer to underlying data of array + PORTABLE_FUNCTION constexpr pointer data() { + return array_detail::bits::ptr_access(arr); + } + + //! get pointer to underlying data of const array + PORTABLE_FUNCTION constexpr const_pointer data() const { + return array_detail::bits::ptr_access(arr); + } + + //! Provides an iterator pointing to the beginning of the array. + PORTABLE_FUNCTION constexpr iterator begin() { return iterator{data()}; } + + //! Provides a const iterator pointing to the beginning of the const array. + PORTABLE_FUNCTION constexpr const_iterator begin() const { + return const_iterator{data()}; + } + + //! Provides a const iterator pointing to the beginning of the const array. + PORTABLE_FUNCTION constexpr const_iterator cbegin() const { + return const_iterator{data()}; + } + + //! Provides an iterator pointing to the end of the array. + PORTABLE_FUNCTION constexpr iterator end() { return iterator{data() + N}; } + + //! Provides a const iterator pointing to the end of the const array. + PORTABLE_FUNCTION constexpr const_iterator end() const { + return const_iterator{data() + N}; + } + + //! Provides a const iterator pointing to the end of the const array. + PORTABLE_FUNCTION constexpr const_iterator cend() const { + return const_iterator{data() + N}; + } + + //! test if array is empty, i.e., N==0 + PORTABLE_FUNCTION constexpr bool empty() const { return N == 0; } + + //! return the size of an array + PORTABLE_FUNCTION constexpr size_type size() const { return N; } + + //! return the maximum size an array can hold + PORTABLE_FUNCTION constexpr size_type max_size() const { return N; } + + //! fill the array with a single value + PORTABLE_FUNCTION constexpr void fill(const_reference value) { + for (auto &element : *this) { + element = value; + } + } + + //! swap the array contents with another + PORTABLE_FUNCTION constexpr void swap(array &other) { + using std::swap; + for (size_type i = 0; i < N; ++i) { + swap(arr[i], other.arr[i]); + } + } +}; + +//! specialization of swap for array +template +PORTABLE_FUNCTION constexpr void swap(array &left, array &right) { + left.swap(right); +} + +//! make_array function to return a deduced type array +// This function was proposed for standardization but superceded by +// C++17 automatic template parameter deduction. It can still be +// useful at C++14 and earlier. +// * This implementation does not support the special handling of +// of reference wrapper + +namespace array_detail { + +// determine the element type of the array +// * if explicitly specified D +// * if not explicitly specified std::common_type_t + +template +struct array_element { + using type = D; +}; + +template +struct array_element : std::common_type {}; + +} // namespace array_detail + +template +PORTABLE_FUNCTION constexpr array::type, + sizeof...(ArgTs)> +make_array(ArgTs &&...args) { + return {{std::forward(args)...}}; +} + +} // end namespace PortsOfCall + +// provide std specializations +namespace std { +/* libc++ and libstdc++ define tuple_size/tuple_element as a class and struct, + * respectively. To prevent clang from issuing a warning that these + * customization points are mismatched, we are temporarily disabling that + * diagnostic. */ +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmismatched-tags" +#endif + +template +struct tuple_size> + : public std::integral_constant {}; + +template +struct tuple_element> { + public: + static_assert(I < N, "index must be less than number of elements"); + + using type = T; +}; + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif +} // namespace std + +#endif // #ifndef _PORTS_OF_CALL_ARRAY_HPP_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e59af456..8e83f110 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -51,4 +51,4 @@ target_link_libraries(test_portsofcall include(Catch) catch_discover_tests(test_portsofcall) -target_sources(test_portsofcall PRIVATE test_portability.cpp) +target_sources(test_portsofcall PRIVATE test_portability.cpp test_array.cpp) diff --git a/test/test_array.cpp b/test/test_array.cpp new file mode 100644 index 00000000..3a9423fc --- /dev/null +++ b/test/test_array.cpp @@ -0,0 +1,278 @@ +#include "ports-of-call/array.hpp" +#include "ports-of-call/portability.hpp" + +#ifndef CATCH_CONFIG_FAST_COMPILE +#define CATCH_CONFIG_FAST_COMPILE +#include +#endif + +#include + +using namespace PortsOfCall; + +TEST_CASE("array nominal element access (GPU)", "[array][GPU]") { + // Declare the array + constexpr int N = 16; + array arr; + + // Fill the array + for (int i = 0; i < N; ++i) { + arr[i] = i + 1; + } + + // Can we read from it on the GPU? + int count = 0; + auto func = PORTABLE_LAMBDA(const int i, int &count) mutable { + if (arr[i] = i + 1) { + ++count; + } + }; + portableReduce("assign_and_check", 0, N, func, count); + CHECK(count == N); +} + +TEST_CASE("array nominal element access", "[array]") { + // Declare the array + constexpr int N = 16; + array arr; + + // Fill the array + for (int i = 0; i < N; ++i) { + arr[i] = i + 1; + } + + // Can we read from it? + for (int i = 0; i < N; ++i) { + CHECK(arr[i] == i + 1); + } +} + +TEST_CASE("array zero-sized element access", "[array]") { + array arr; + + // accessing the zeroth element of a zero-size array is + // allowed but undefined behavior. We will only verify that + // the address returned is well defined. + CHECK(std::addressof(arr[0])); +} + +TEST_CASE("const array element access", "[array]") { + // Declare the array + constexpr int N = 5; + array const arr{{0, 1, 2, 3, 4}}; + + // Can we read from it? + for (int i = 0; i < N; ++i) { + CHECK(arr[i] == i); + } +} + +TEST_CASE("const array zero-sized element access", "[array]") { + array const arr{}; + + // accessing the zeroth element of a zero-size array is + // allowed but undefined behavior. We will only verify that + // the address returned is well defined. + CHECK(std::addressof(arr[0])); +} + +TEST_CASE("array range-based for loop", "[array]") { + SECTION("with non-const array") { + // Declare the array + constexpr int N = 15; + array arr; + + // Fill the loop + int i = 0; + for (auto &x : arr) { + x = ++i; + } + + // Check the values + i = 0; + for (auto &x : arr) { + ++i; + CHECK(x == i); + } + } + + SECTION("with const array") { + // Declare the array + constexpr int N = 5; + array const arr{{1, 2, 3, 4, 5}}; + + // Check the values + int i = 0; + for (auto const &x : arr) { + ++i; + CHECK(x == i); + } + } +} + +TEST_CASE("array begins and ends", "[array]") { + using std::begin; + using std::cbegin; + using std::cend; + using std::end; + + SECTION("with non-const array") { + array arr{{1, 2, 3}}; + + CHECK(std::is_same::value); + CHECK(std::is_same::value); + + CHECK(std::is_same::value); + CHECK(std::is_same::value); + } + + SECTION("with const array") { + array const arr{{1, 2, 3}}; + + CHECK(std::is_same::value); + CHECK(std::is_same::value); + + CHECK(std::is_same::value); + CHECK(std::is_same::value); + } + + SECTION("with non-const zero-sized array") { + array arr; + + CHECK(begin(arr) == end(arr)); + } + + SECTION("with const zero-sized array") { + array const arr{}; + + CHECK(begin(arr) == end(arr)); + } +} + +TEST_CASE("array front and back", "[array]") { + array arr1{{3, 2, 1}}; + + CHECK(std::is_same::value); + CHECK(arr1.front() == 3); + + CHECK(std::is_same::value); + CHECK(arr1.back() == 1); + + array const arr2{{3, 2, 1}}; + + CHECK(std::is_same::value); + CHECK(arr2.front() == 3.0); + + CHECK(std::is_same::value); + CHECK(arr2.back() == 1.0); +} + +TEST_CASE("array data", "[array]") { + SECTION("with non-const array") { + array arr{{3, 2, 1}}; + + CHECK(std::is_same::value); + CHECK(arr.data() == std::addressof(arr[0])); + } + + SECTION("with non-const array") { + array const arr{{3, 2, 1}}; + + CHECK(std::is_same::value); + CHECK(arr.data() == std::addressof(arr[0])); + } +} + +TEST_CASE("array empty", "[array]") { + SECTION("with non-empty array") { + array arr; + + CHECK(not arr.empty()); + } + + SECTION("with empty array") { + array arr; + + CHECK(arr.empty()); + } +} + +TEST_CASE("array sizes", "[array]") { + array arr; + + CHECK(arr.size() == arr.max_size()); +} + +TEST_CASE("array fill (GPU)", "[array][GPU]") { + constexpr std::size_t N = 42; + std::size_t count = 0; + auto func = PORTABLE_LAMBDA(const int i, std::size_t &count) { + constexpr double value = 3.14; + array arr; + arr.fill(value); + for (const double x : arr) { + count += (x == value) ? 1 : 0; + } + }; + constexpr std::size_t M = 5; + portableReduce("check", 0, M, func, count); + CHECK(count == N * M); +} + +TEST_CASE("array fill", "[array]") { + array arr; + arr.fill(3.14); + + for (auto const &x : arr) { + CHECK(x == 3.14); + } +} + +TEST_CASE("array swap", "[array]") { + array zeros; + zeros.fill(0); + array ones; + ones.fill(1); + + zeros.swap(ones); + + for (auto const &x : zeros) { + CHECK(x == 1); + } + + for (auto const &x : ones) { + CHECK(x == 0); + } + + using std::swap; + + swap(zeros, ones); + + for (auto const &x : zeros) { + CHECK(x == 0); + } + + for (auto const &x : ones) { + CHECK(x == 1); + } +} + +TEST_CASE("array tuple_size", "[array]") { + array arr; + + CHECK(std::tuple_size::value == 5); +} + +TEST_CASE("array tuple_element", "[array]") { + struct Foo {}; + + array foos; + + CHECK(std::is_same::type>::value); +} + +TEST_CASE("make_array function", "[array]") { + auto arr = make_array(1.0, 2.0, 3.0); + + CHECK(std::is_same>::value); +}