From 390d6bbae3b58299541f9fe7ea39883a3decf376 Mon Sep 17 00:00:00 2001 From: Rithika SridharBabu Date: Fri, 23 Oct 2026 15:39:06 -0700 Subject: [PATCH] Avoid temporary arrays for independent numeric generators --- docs/source/builder.rst | 16 ++++ include/xtensor/core/xsemantic.hpp | 15 +++- include/xtensor/generators/xbuilder.hpp | 6 ++ include/xtensor/generators/xgenerator.hpp | 5 ++ include/xtensor/utils/xutils.hpp | 10 +++ test/test_xassign.cpp | 101 ++++++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) diff --git a/docs/source/builder.rst b/docs/source/builder.rst index 04b193def..ee622b4f6 100644 --- a/docs/source/builder.rst +++ b/docs/source/builder.rst @@ -26,6 +26,22 @@ Numerical ranges - :cpp:func:`xt::linspace(start, stop, num_samples) `: generates num_samples evenly spaced numbers over given interval. - :cpp:func:`xt::logspace(start, stop, num_samples) `: generates num_samples evenly spaced on a log scale over given interval +Assignment of independent generators +----------------------------------- + +Assigning ``arange`` or ``linspace`` with arithmetic types to a contiguous +container reuses its storage when the size is unchanged, without creating a +temporary array. Views retain their existing assignment path, which can benefit +from generating into a contiguous temporary first. + +Custom generator functors conservatively retain overlap protection. A functor +which cannot read array storage can opt in by specializing +``xt::is_memory_independent`` to inherit from ``std::true_type``. +``xgenerator`` forwards this trait from its functor, including reference-qualified +functors. Do not opt in for functors that read other expressions, since those +expressions can refer to the assignment destination. The option +``XTENSOR_FORCE_TEMPORARY_MEMORY_IN_ASSIGNMENTS`` still forces temporary evaluation. + Joining expressions ------------------- diff --git a/include/xtensor/core/xsemantic.hpp b/include/xtensor/core/xsemantic.hpp index 05b3248c0..593893cab 100644 --- a/include/xtensor/core/xsemantic.hpp +++ b/include/xtensor/core/xsemantic.hpp @@ -682,7 +682,20 @@ namespace xt template inline auto xcontainer_semantic::operator=(const xexpression& e) -> derived_type& { - return base_type::operator=(e); +#ifndef XTENSOR_FORCE_TEMPORARY_MEMORY_IN_ASSIGNMENTS + // Keep this optimization on containers: a view may benefit from first + // generating into a contiguous temporary with a specialized assign_to. + if constexpr (is_memory_independent::value && D::contiguous_layout) + { + // Validate the shape before entering a generator's noexcept assign_to. + this->derived_cast().resize(e.derived_cast().shape()); + return this->assign(e); + } + else +#endif + { + return base_type::operator=(e); + } } /********************************* diff --git a/include/xtensor/generators/xbuilder.hpp b/include/xtensor/generators/xbuilder.hpp index 1026b21c7..e40734865 100644 --- a/include/xtensor/generators/xbuilder.hpp +++ b/include/xtensor/generators/xbuilder.hpp @@ -390,6 +390,12 @@ namespace xt }; } + template + struct is_memory_independent> + : std::conjunction, std::is_arithmetic, std::is_arithmetic> + { + }; + /** * Generates an array with ones on the diagonal. * @param shape shape of the resulting expression diff --git a/include/xtensor/generators/xgenerator.hpp b/include/xtensor/generators/xgenerator.hpp index 57d7d5d6c..a4166cb1c 100644 --- a/include/xtensor/generators/xgenerator.hpp +++ b/include/xtensor/generators/xgenerator.hpp @@ -58,6 +58,11 @@ namespace xt template class xgenerator; + template + struct is_memory_independent> : is_memory_independent> + { + }; + template concept xgenerator_concept = is_specialization_of>::value; diff --git a/include/xtensor/utils/xutils.hpp b/include/xtensor/utils/xutils.hpp index ed4a04f48..28288a768 100644 --- a/include/xtensor/utils/xutils.hpp +++ b/include/xtensor/utils/xutils.hpp @@ -672,6 +672,16 @@ namespace xt return !(a == b); } + /** + * Whether evaluating a functor or expression is independent of array storage. + * Specialize only for types which cannot read from an assignment destination. + * Unknown types conservatively retain overlap protection. + */ + template + struct is_memory_independent : std::false_type + { + }; + /***************** * assignable_to * ******************/ diff --git a/test/test_xassign.cpp b/test/test_xassign.cpp index 3535b4886..9e068f14d 100644 --- a/test/test_xassign.cpp +++ b/test/test_xassign.cpp @@ -8,6 +8,7 @@ ****************************************************************************/ #include +#include #include #include @@ -15,11 +16,58 @@ #include "xtensor/containers/xtensor.hpp" #include "xtensor/core/xassign.hpp" #include "xtensor/core/xnoalias.hpp" +#include "xtensor/generators/xbuilder.hpp" #include "xtensor/misc/xmanipulation.hpp" #include "test_common.hpp" #include "test_common_macros.hpp" +namespace +{ + template + struct counting_allocator : std::allocator + { + using value_type = T; + inline static std::size_t allocations = 0; + + template + struct rebind + { + using other = counting_allocator; + }; + + counting_allocator() = default; + + template + counting_allocator(const counting_allocator&) + { + } + + T* allocate(std::size_t n) + { + ++allocations; + return std::allocator::allocate(n); + } + }; + + struct reverse_reader + { + using value_type = int; + const xt::xarray& source; + + int operator()(std::size_t i) const + { + return source(source.size() - 1 - i); + } + + template + int element(It first, It) const + { + return (*this)(*first); + } + }; +} + // a dummy shape *not derived* from std::vector but compatible template class my_vector @@ -137,6 +185,57 @@ class my_vector namespace xt { +#ifndef XTENSOR_FORCE_TEMPORARY_MEMORY_IN_ASSIGNMENTS + TEST(xassign, independent_generator_reuses_storage) + { + using allocator = counting_allocator; + using array = xtensor; + array a = array::from_shape({16}); + + allocator::allocations = 0; + a = arange(16); + EXPECT_EQ(allocator::allocations, 0u); + for (std::size_t i = 0; i < a.size(); ++i) + { + EXPECT_EQ(a(i), double(i)); + } + + allocator::allocations = 0; + a = linspace(0., 15., 16); + EXPECT_EQ(allocator::allocations, 0u); + for (std::size_t i = 0; i < a.size(); ++i) + { + EXPECT_EQ(a(i), double(i)); + } + } +#endif + + TEST(xassign, independent_generator_resizes) + { + xarray a = {{-1., -1.}, {-1., -1.}}; + a = arange(5); + EXPECT_EQ(a.dimension(), 1u); + EXPECT_EQ(a.size(), 5u); + EXPECT_EQ(a(4), 4.); + + a = arange(0); + EXPECT_EQ(a.size(), 0u); + a = linspace(2., 4., 3); + EXPECT_EQ(a, (xarray{2., 3., 4.})); + } + + TEST(xassign, reading_generators_keep_overlap_protection) + { + xarray a = {1, 2, 3, 4}; + auto reverse = detail::make_xgenerator(reverse_reader{a}, {4}); + static_assert(!is_memory_independent::value); + a = reverse; + EXPECT_EQ(a, (xarray{4, 3, 2, 1})); + + a = concatenate(xtuple(a, a)); + EXPECT_EQ(a, (xarray{4, 3, 2, 1, 4, 3, 2, 1})); + } + template struct rebind_container> { @@ -173,5 +272,7 @@ namespace xt { auto a = xt::xtensor::from_shape({2}); XT_ASSERT_THROW(a = xt::expand_dims(a, 0), std::runtime_error); + auto b = xt::xtensor::from_shape({2, 2}); + XT_ASSERT_THROW(b = arange(4), std::runtime_error); } }