From 1254a113714966eb52a214ce8dd61229530b3b3d Mon Sep 17 00:00:00 2001 From: Dillon Sharlet Date: Wed, 15 Sep 2021 01:33:46 -0600 Subject: [PATCH 1/6] Start of FAQs --- test/faq.cpp | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/faq.cpp diff --git a/test/faq.cpp b/test/faq.cpp new file mode 100644 index 00000000..853da5a0 --- /dev/null +++ b/test/faq.cpp @@ -0,0 +1,141 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "array.h" +#include "test.h" + +#include +#include + +namespace nda { + +TEST(faq_carray) { + // Q: How do I declare an array with the same memory layout as a C + // multidimensional array? + + // A: The ordering of the dimensions in memory is reversed relative to + // the declaration order. + + // To demonstrate this, we can construct a 3-dimensional array in C, + // where each element of the array is equal to its indices: + constexpr int width = 5; + constexpr int height = 4; + constexpr int depth = 3; + std::tuple carray[depth][height][width]; + for (int z = 0; z < depth; z++) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + carray[z][y][x] = std::make_tuple(x, y, z); + } + } + } + + // Now we can create an array_ref of this carray, and check that + // the coordinates map to the same location: + dense_array_ref, 3> carray_ref(&carray[0][0][0], {width, height, depth}); + for_each_index(carray_ref.shape(), [&](std::tuple i) { + ASSERT_EQ(carray_ref[i], i); + }); + + // Q: That array generates less efficient code than my C array! + + // A: To match the generated code from C, we need to give the + // compiler more information, like the C array declaration does. + + // This C array is equivalent to the following shape, fully known at + // compile time: + using CArrayShape = + shape, dim<0, height, width>, dim<0, depth, width*height>>; + + // Now we can create an array_ref of this C array, and check that + // the coordinates map to the same location: + array_ref, CArrayShape> carray_ref_fixed(&carray[0][0][0]); + for_each_index(carray_ref_fixed.shape(), [&](std::tuple i) { + ASSERT_EQ(carray_ref_fixed[i], i); + }); +} + +TEST(faq_crop) { + // Q: Cropping in this library is weird. How do I crop an array the way + // (my favorite library) does it? + + // A: After cropping, the resulting array will have a min corresponding + // to the cropped region: + dense_array array({100}); + for (int i = 0; i < array.size(); i++) { + array(i) = i; + } + const int crop_begin = 25; + const int crop_end = 50; + auto cropped = array(r(crop_begin, crop_end)); + for (int i = crop_begin; i < crop_end; i++) { + ASSERT_EQ(array(i), cropped(i)); + } + + // This differs from most alternative libraries. To match this behavior, + // the min of the resulting cropped array needs to be changed to 0: + cropped.shape().dim<0>().set_min(0); + for (int i = crop_begin; i < crop_end; i++) { + ASSERT_EQ(array(i), cropped(i - crop_begin)); + } + + // The reason array works this way is to enable transparent tiling + // optimizations of algorithms. +} + +TEST(faq_move_reinterpret_shape) { + // Q: array's move constructor requires the source array to have the same + // shape type. How do I move ownership to an array of a different shape type? + + // A: The helper function move_reinterpret_shape does this: + array_of_rank source({3, 4, 5}); + dense_array dest = move_reinterpret_shape>(std::move(source)); + + // This can fail at runtime if the source shape is not compatible with the + // destination shape. +} + +TEST(faq_stack_allocation) { + // Q: How do I allocate an array on the stack? + + // A: Use `auto_allocator<>` as the allocator for your arrays. + + // Define an allocator that has storage for up to 100 elements. + using StackAllocator = auto_allocator; + ASSERT(sizeof(StackAllocator) > sizeof(int) * 100); + dense_array stack_array({2, 5, 10}); + // Check that the data in the array is the same as the address of the + // array itself. + ASSERT_EQ(stack_array.base(), reinterpret_cast(&stack_array)); + + // Q: My array still isn't being allocated on the stack! Why not? + + // A: If the array is too big to fit in the allocator, it will use the + // `BaseAlloc` of `auto_allocator`, which is `std::allocator` by default: + dense_array not_stack_array({3, 5, 10}); + ASSERT(not_stack_array.base() != reinterpret_cast(¬_stack_array)); +} + +TEST(faq_no_initialization) { + // Q: When I declare an array, the memory is being initialized. I'd rather + // not incur the cost of initialization, how can I avoid this? + + // A: Use `uninitialized_std_allocator<>` as the allocator for your arrays. + + // Define an allocator that will not default construct the values. + using UninitialiezdAllocator = uninitialized_std_allocator; + dense_array stack_array({2, 5, 10}); +} + +} // namespace nda From 9cb7467cbede7a4643afb10b098c7be8457d5e5d Mon Sep 17 00:00:00 2001 From: Dillon Sharlet Date: Wed, 15 Sep 2021 15:18:28 -0600 Subject: [PATCH 2/6] Improve FAQs for reshape related helpers (#50). --- array.h | 7 ++-- test/faq.cpp | 106 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/array.h b/array.h index f209a1e9..6c7d9fab 100644 --- a/array.h +++ b/array.h @@ -2434,9 +2434,9 @@ class array { * by `offset`. This function is disabled for non-trivial types, because it * does not call the destructor or constructor for newly inaccessible or newly * accessible elements, respectively. */ - void set_shape(const Shape& new_shape, index_t offset = 0) { + void set_shape(Shape new_shape, index_t offset = 0) { static_assert(std::is_trivial::value, "set_shape is broken for non-trivial types."); - assert(new_shape.is_resolved()); + new_shape.resolve(); assert(new_shape.is_subset_of(shape_, -offset)); shape_ = new_shape; base_ = internal::pointer_add(base_, offset); @@ -2761,7 +2761,8 @@ const_array_ref reinterpret(const array& a) { * `new_shape`, with a base pointer offset `offset`. */ template NDARRAY_HOST_DEVICE array_ref reinterpret_shape( - const array_ref& a, const NewShape& new_shape, index_t offset = 0) { + const array_ref& a, NewShape new_shape, index_t offset = 0) { + new_shape.resolve(); assert(new_shape.is_subset_of(a.shape(), -offset)); return array_ref(a.base() + offset, new_shape); } diff --git a/test/faq.cpp b/test/faq.cpp index 853da5a0..7faf647b 100644 --- a/test/faq.cpp +++ b/test/faq.cpp @@ -20,8 +20,8 @@ namespace nda { -TEST(faq_carray) { - // Q: How do I declare an array with the same memory layout as a C +TEST(faq_c_array) { + // Q: How do I declare an `array` with the same memory layout as a C // multidimensional array? // A: The ordering of the dimensions in memory is reversed relative to @@ -32,20 +32,20 @@ TEST(faq_carray) { constexpr int width = 5; constexpr int height = 4; constexpr int depth = 3; - std::tuple carray[depth][height][width]; + std::tuple c_array[depth][height][width]; for (int z = 0; z < depth; z++) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - carray[z][y][x] = std::make_tuple(x, y, z); + c_array[z][y][x] = std::make_tuple(x, y, z); } } } - // Now we can create an array_ref of this carray, and check that + // Now we can create an array_ref of this c_array, and check that // the coordinates map to the same location: - dense_array_ref, 3> carray_ref(&carray[0][0][0], {width, height, depth}); - for_each_index(carray_ref.shape(), [&](std::tuple i) { - ASSERT_EQ(carray_ref[i], i); + dense_array_ref, 3> c_array_ref(&c_array[0][0][0], {width, height, depth}); + for_each_index(c_array_ref.shape(), [&](std::tuple i) { + ASSERT_EQ(c_array_ref[i], i); }); // Q: That array generates less efficient code than my C array! @@ -60,12 +60,82 @@ TEST(faq_carray) { // Now we can create an array_ref of this C array, and check that // the coordinates map to the same location: - array_ref, CArrayShape> carray_ref_fixed(&carray[0][0][0]); - for_each_index(carray_ref_fixed.shape(), [&](std::tuple i) { - ASSERT_EQ(carray_ref_fixed[i], i); + array_ref, CArrayShape> c_array_ref_fixed(&c_array[0][0][0]); + for_each_index(c_array_ref_fixed.shape(), [&](std::tuple i) { + ASSERT_EQ(c_array_ref_fixed[i], i); }); } +TEST(faq_reshape) { + // Q: How do I resize or change the shape of an already constructed array? + + // A: There are several options, with different behaviors. First, we can use + // `array::reshape`, which changes the shape of an array while moving the + // elements of the intersection of the old shape and new shape to the new + // array: + dense_array a({3, 4}); + for_all_indices(a.shape(), [&](int x, int y) { + a(x, y) = y * 3 + x; + }); + for (auto y : a.y()) { + for (auto x : a.x()) { + std::cout << a(x, y) << " "; + } + std::cout << std::endl; + } + std::cout << std::endl; + // Output: + // 0 1 2 + // 3 4 5 + // 6 7 8 + // 9 10 11 + + a.reshape({2, 6}); + for (auto y : a.y()) { + for (auto x : a.x()) { + std::cout << a(x, y) << " "; + } + std::cout << std::endl; + } + std::cout << std::endl; + // Output: + // 0 1 + // 3 4 + // 6 7 + // 9 10 + // 0 0 + // 0 0 + + // A: We can also reinterpret the shape of an existing array using `array::set_shape`: + a.set_shape({4, 3}); + for (auto y : a.y()) { + for (auto x : a.x()) { + std::cout << a(x, y) << " "; + } + std::cout << std::endl; + } + std::cout << std::endl; + // Output: + // 0 1 3 4 + // 6 7 9 10 + // 0 0 0 0 + + // A: We can also use `reinterpret_shape` to make a new `array_ref` with the + // new shape: + auto a_reshaped = reinterpret_shape(a, dense_shape<2>{4, 3}); + ASSERT(a_reshaped == a); + + // Q: `array`'s move constructor requires the source array to have the same + // shape type. How do I move ownership to an array of a different shape type? + + // A: The helper function `move_reinterpret_shape` does this: + array_of_rank source({3, 4, 5}); + dense_array dest = move_reinterpret_shape>(std::move(source)); + + // This can fail at runtime if the source shape is not compatible with the + // destination shape. +} + TEST(faq_crop) { // Q: Cropping in this library is weird. How do I crop an array the way // (my favorite library) does it? @@ -94,18 +164,6 @@ TEST(faq_crop) { // optimizations of algorithms. } -TEST(faq_move_reinterpret_shape) { - // Q: array's move constructor requires the source array to have the same - // shape type. How do I move ownership to an array of a different shape type? - - // A: The helper function move_reinterpret_shape does this: - array_of_rank source({3, 4, 5}); - dense_array dest = move_reinterpret_shape>(std::move(source)); - - // This can fail at runtime if the source shape is not compatible with the - // destination shape. -} - TEST(faq_stack_allocation) { // Q: How do I allocate an array on the stack? @@ -128,7 +186,7 @@ TEST(faq_stack_allocation) { } TEST(faq_no_initialization) { - // Q: When I declare an array, the memory is being initialized. I'd rather + // Q: When I declare an `array`, the memory is being initialized. I'd rather // not incur the cost of initialization, how can I avoid this? // A: Use `uninitialized_std_allocator<>` as the allocator for your arrays. From 265a7d3f33bf73f906cd52f27c00ada3f2d0d7af Mon Sep 17 00:00:00 2001 From: Dillon Sharlet Date: Tue, 19 Oct 2021 18:46:21 -0700 Subject: [PATCH 3/6] More faq improvements. --- test/faq.cpp | 69 ++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/test/faq.cpp b/test/faq.cpp index 7faf647b..8f54304f 100644 --- a/test/faq.cpp +++ b/test/faq.cpp @@ -15,9 +15,6 @@ #include "array.h" #include "test.h" -#include -#include - namespace nda { TEST(faq_c_array) { @@ -42,28 +39,12 @@ TEST(faq_c_array) { } // Now we can create an array_ref of this c_array, and check that - // the coordinates map to the same location: + // the coordinates map to the same location, indicating that we used + // the same convention to determine strides as the C compiler: dense_array_ref, 3> c_array_ref(&c_array[0][0][0], {width, height, depth}); for_each_index(c_array_ref.shape(), [&](std::tuple i) { ASSERT_EQ(c_array_ref[i], i); }); - - // Q: That array generates less efficient code than my C array! - - // A: To match the generated code from C, we need to give the - // compiler more information, like the C array declaration does. - - // This C array is equivalent to the following shape, fully known at - // compile time: - using CArrayShape = - shape, dim<0, height, width>, dim<0, depth, width*height>>; - - // Now we can create an array_ref of this C array, and check that - // the coordinates map to the same location: - array_ref, CArrayShape> c_array_ref_fixed(&c_array[0][0][0]); - for_each_index(c_array_ref_fixed.shape(), [&](std::tuple i) { - ASSERT_EQ(c_array_ref_fixed[i], i); - }); } TEST(faq_reshape) { @@ -72,7 +53,7 @@ TEST(faq_reshape) { // A: There are several options, with different behaviors. First, we can use // `array::reshape`, which changes the shape of an array while moving the // elements of the intersection of the old shape and new shape to the new - // array: + // array, similar to `std::vector::resize`: dense_array a({3, 4}); for_all_indices(a.shape(), [&](int x, int y) { a(x, y) = y * 3 + x; @@ -85,10 +66,10 @@ TEST(faq_reshape) { } std::cout << std::endl; // Output: - // 0 1 2 - // 3 4 5 - // 6 7 8 - // 9 10 11 + // 0 1 2 + // 3 4 5 + // 6 7 8 + // 9 10 11 a.reshape({2, 6}); for (auto y : a.y()) { @@ -99,14 +80,17 @@ TEST(faq_reshape) { } std::cout << std::endl; // Output: - // 0 1 - // 3 4 - // 6 7 - // 9 10 - // 0 0 - // 0 0 - - // A: We can also reinterpret the shape of an existing array using `array::set_shape`: + // 0 1 + // 3 4 + // 6 7 + // 9 10 + // 0 0 + // 0 0 + // Observe that the right column of the original array has been lost, and + // two default-constructed rows have been added to the bottom of the array. + + // A: We can also reinterpret the shape of an existing array using + // `array::set_shape`: a.set_shape({4, 3}); for (auto y : a.y()) { for (auto x : a.x()) { @@ -116,9 +100,11 @@ TEST(faq_reshape) { } std::cout << std::endl; // Output: - // 0 1 3 4 - // 6 7 9 10 - // 0 0 0 0 + // 0 1 3 4 + // 6 7 9 10 + // 0 0 0 0 + // Observe that this has not removed or added any values from the array, the + // underlying memory has simply be reinterpreted as a different array. // A: We can also use `reinterpret_shape` to make a new `array_ref` with the // new shape: @@ -128,7 +114,8 @@ TEST(faq_reshape) { // Q: `array`'s move constructor requires the source array to have the same // shape type. How do I move ownership to an array of a different shape type? - // A: The helper function `move_reinterpret_shape` does this: + // A: The helper function `move_reinterpret_shape` combines move construction + // with `reinterpret_shape`: array_of_rank source({3, 4, 5}); dense_array dest = move_reinterpret_shape>(std::move(source)); @@ -154,7 +141,7 @@ TEST(faq_crop) { } // This differs from most alternative libraries. To match this behavior, - // the min of the resulting cropped array needs to be changed to 0: + // the `min` of the resulting cropped array needs to be changed to 0: cropped.shape().dim<0>().set_min(0); for (int i = crop_begin; i < crop_end; i++) { ASSERT_EQ(array(i), cropped(i - crop_begin)); @@ -190,10 +177,8 @@ TEST(faq_no_initialization) { // not incur the cost of initialization, how can I avoid this? // A: Use `uninitialized_std_allocator<>` as the allocator for your arrays. - - // Define an allocator that will not default construct the values. using UninitialiezdAllocator = uninitialized_std_allocator; - dense_array stack_array({2, 5, 10}); + dense_array uninitialized_array({2, 5, 10}); } } // namespace nda From 2ebd74490f9a9a6fb4fb6452a6ef294dbeb1f8f4 Mon Sep 17 00:00:00 2001 From: Dillon Date: Thu, 24 Mar 2022 20:59:38 -0700 Subject: [PATCH 4/6] Update test/faq.cpp Co-authored-by: Jiawen (Kevin) Chen --- test/faq.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/faq.cpp b/test/faq.cpp index 8f54304f..58dd9c76 100644 --- a/test/faq.cpp +++ b/test/faq.cpp @@ -177,8 +177,8 @@ TEST(faq_no_initialization) { // not incur the cost of initialization, how can I avoid this? // A: Use `uninitialized_std_allocator<>` as the allocator for your arrays. - using UninitialiezdAllocator = uninitialized_std_allocator; - dense_array uninitialized_array({2, 5, 10}); + using UninitializedAllocator = uninitialized_std_allocator; + dense_array uninitialized_array({2, 5, 10}); } } // namespace nda From 99082c2e592be044d8eea6120900b3bbc67d5f6f Mon Sep 17 00:00:00 2001 From: dsharlet Date: Thu, 24 Mar 2022 22:23:28 -0700 Subject: [PATCH 5/6] Add docs about allocator compatibility --- array.h | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/array.h b/array.h index c3e55bba..fcb092da 100644 --- a/array.h +++ b/array.h @@ -2028,7 +2028,9 @@ using dense_array_ref = array_ref>; template using const_dense_array_ref = dense_array_ref; -/** A multi-dimensional array container that owns an allocation of memory. */ +/** A multi-dimensional array container that owns an allocation of memory. `Alloc` is + * an allocator that can be an STL allocator type such as `std::allocator`. However, + * unlike STL allocators, it may be stateful. */ template > class array { public: @@ -2815,10 +2817,13 @@ auto reorder(const array& a) { return reinterpret_shape(a, reorder(a.shape())); } -/** Allocator satisfying the `std::allocator` interface that owns a buffer with - * automatic storage, and a fallback base allocator. For allocations, the - * allocator uses the buffer if it is large enough and not already allocated, - * otherwise it uses the base allocator. */ +/** Allocator for use with `array` that owns a buffer with automatic storage, + * and a fallback base allocator. When allocating memory, this allocator uses + * the buffer if it is large enough and not already allocated, otherwise it + * uses the base allocator. + * + * While this allocator appears to be compatible with `std::allocator`, it is + * not safe to use with STL containers. */ template > class auto_allocator { alignas(Alignment) char buffer[N * sizeof(T)]; From 20f1743a344f4e121f1163dc52fb9131b2c3fcc2 Mon Sep 17 00:00:00 2001 From: dsharlet Date: Thu, 24 Mar 2022 22:23:40 -0700 Subject: [PATCH 6/6] Improve description of dimension ordering --- test/faq.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/faq.cpp b/test/faq.cpp index 58dd9c76..f90f259b 100644 --- a/test/faq.cpp +++ b/test/faq.cpp @@ -21,8 +21,9 @@ TEST(faq_c_array) { // Q: How do I declare an `array` with the same memory layout as a C // multidimensional array? - // A: The ordering of the dimensions in memory is reversed relative to - // the declaration order. + // A: In C, the last dimension is the "innermost" dimension, the dimension + // with the smallest stride. In `array`, the first dimension is the innermost + // dimension, i.e. Fortran ordering. // To demonstrate this, we can construct a 3-dimensional array in C, // where each element of the array is equal to its indices: