From c11f017461f55926211803a376ec695694bd960c Mon Sep 17 00:00:00 2001 From: Mikael Simberg Date: Mon, 25 Nov 2024 10:48:35 +0100 Subject: [PATCH] Store receiver in any_operation_state to avoid dynamic allocation for receiver --- .../pika/execution_base/any_sender.hpp | 358 +++++++++--------- libs/pika/execution_base/src/any_sender.cpp | 11 +- 2 files changed, 175 insertions(+), 194 deletions(-) diff --git a/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp b/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp index 51beea7dad..d3ddf62e67 100644 --- a/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp +++ b/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp @@ -12,12 +12,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include @@ -364,14 +366,15 @@ namespace pika::detail { } // namespace pika::detail namespace pika::execution::experimental::detail { - struct any_operation_state_base + struct PIKA_EXPORT any_operation_state_holder_base { - virtual ~any_operation_state_base() noexcept = default; - virtual bool empty() const noexcept { return false; } + virtual ~any_operation_state_holder_base() noexcept = default; + virtual bool empty() const noexcept; virtual void start() & noexcept = 0; }; - struct PIKA_EXPORT empty_any_operation_state final : any_operation_state_base + struct PIKA_EXPORT empty_any_operation_state_holder_state final + : any_operation_state_holder_base { bool empty() const noexcept override; void start() & noexcept override; @@ -380,204 +383,108 @@ namespace pika::execution::experimental::detail { namespace pika::detail { template <> - struct empty_vtable_type + struct empty_vtable_type { - using type = pika::execution::experimental::detail::empty_any_operation_state; + using type = pika::execution::experimental::detail::empty_any_operation_state_holder_state; }; } // namespace pika::detail namespace pika::execution::experimental::detail { - template - struct any_operation_state_impl final : any_operation_state_base - { - std::decay_t> operation_state; - - template - any_operation_state_impl(Sender_&& sender, Receiver_&& receiver) - : operation_state(pika::execution::experimental::connect( - std::forward(sender), std::forward(receiver))) - { - } - ~any_operation_state_impl() noexcept = default; - - void start() & noexcept override { pika::execution::experimental::start(operation_state); } - }; - - class PIKA_EXPORT any_operation_state - { - using base_type = detail::any_operation_state_base; - template - using impl_type = detail::any_operation_state_impl; - using storage_type = pika::detail::movable_sbo_storage; - - storage_type storage{}; - - public: - template - any_operation_state(Sender&& sender, Receiver&& receiver) - { - storage.template store>( - std::forward(sender), std::forward(receiver)); - } - - ~any_operation_state() noexcept = default; - any_operation_state(any_operation_state&&) = delete; - any_operation_state(any_operation_state const&) = delete; - any_operation_state& operator=(any_operation_state&&) = delete; - any_operation_state& operator=(any_operation_state const&) = delete; - - PIKA_EXPORT friend void tag_invoke( - pika::execution::experimental::start_t, any_operation_state& os) noexcept; - }; - - template - struct any_receiver_base - { - virtual ~any_receiver_base() = default; - virtual void move_into(void* p) = 0; - virtual void set_value(Ts... ts) && = 0; - virtual void set_error(std::exception_ptr ep) && noexcept = 0; - virtual void set_stopped() && noexcept = 0; - virtual bool empty() const noexcept { return false; } - }; - - [[noreturn]] PIKA_EXPORT void throw_bad_any_call( - char const* class_name, char const* function_name); - template - struct empty_any_receiver final : any_receiver_base + struct any_receiver_ref_base { - void move_into(void*) override { PIKA_UNREACHABLE; } - - bool empty() const noexcept override { return true; } + void* receiver = nullptr; - void set_value(Ts...) && override { throw_bad_any_call("any_receiver", "set_value"); } - - [[noreturn]] void set_error(std::exception_ptr) && noexcept override + template + any_receiver_ref_base(Receiver* receiver) + : receiver(static_cast(receiver)) { - throw_bad_any_call("any_receiver", "set_error"); } + any_receiver_ref_base(any_receiver_ref_base&&) = default; + any_receiver_ref_base& operator=(any_receiver_ref_base&&) = default; + any_receiver_ref_base(any_receiver_ref_base const&) = delete; + any_receiver_ref_base& operator=(any_receiver_ref_base const&) = delete; - [[noreturn]] void set_stopped() && noexcept override - { - throw_bad_any_call("any_receiver", "set_stopped"); - } + virtual void set_value(Ts...) noexcept = 0; + virtual void set_error(std::exception_ptr) noexcept = 0; + virtual void set_stopped() noexcept = 0; }; -} // namespace pika::execution::experimental::detail -namespace pika::detail { - template - struct empty_vtable_type> - { - using type = pika::execution::experimental::detail::empty_any_receiver; - }; -} // namespace pika::detail - -namespace pika::execution::experimental::detail { template - struct any_receiver_impl final : any_receiver_base + struct any_receiver_ref : any_receiver_ref_base { - std::decay_t receiver; + using any_receiver_ref_base::receiver; - template , any_receiver_impl>>> - explicit any_receiver_impl(Receiver_&& receiver) - : receiver(std::forward(receiver)) + template + any_receiver_ref(Receiver_* receiver) + : any_receiver_ref_base(receiver) { } + any_receiver_ref(any_receiver_ref&&) = default; + any_receiver_ref& operator=(any_receiver_ref&&) = default; + any_receiver_ref(any_receiver_ref const&) = delete; + any_receiver_ref& operator=(any_receiver_ref const&) = delete; - void move_into(void* p) override { new (p) any_receiver_impl(std::move(receiver)); } - - void set_value(Ts... ts) && override + void set_value(Ts... ts) noexcept override { - pika::execution::experimental::set_value(std::move(receiver), std::move(ts)...); + pika::execution::experimental::set_value( + std::move(*static_cast*>(receiver)), + std::forward(ts)...); } - void set_error(std::exception_ptr ep) && noexcept override + void set_error(std::exception_ptr ep) noexcept override { - pika::execution::experimental::set_error(std::move(receiver), std::move(ep)); + pika::execution::experimental::set_error( + std::move(*static_cast*>(receiver)), std::move(ep)); } - void set_stopped() && noexcept override + void set_stopped() noexcept override { - pika::execution::experimental::set_stopped(std::move(receiver)); + pika::execution::experimental::set_stopped( + std::move(*static_cast*>(receiver))); } }; template - class any_receiver + struct any_receiver { - using base_type = detail::any_receiver_base; - template - using impl_type = detail::any_receiver_impl; - using storage_type = pika::detail::movable_sbo_storage; - - storage_type storage{}; - - public: PIKA_STDEXEC_RECEIVER_CONCEPT - template , any_receiver>>> - explicit any_receiver(Receiver&& receiver) - { - storage.template store>(std::forward(receiver)); - } - template , any_receiver>>> - any_receiver& operator=(Receiver&& receiver) + any_receiver_ref_base* receiver; + + any_receiver(any_receiver_ref_base* receiver) + : receiver(receiver) { - storage.template store>(std::forward(receiver)); - return *this; } - - ~any_receiver() = default; any_receiver(any_receiver&&) = default; - any_receiver(any_receiver const&) = delete; any_receiver& operator=(any_receiver&&) = default; + any_receiver(any_receiver const&) = delete; any_receiver& operator=(any_receiver const&) = delete; template - auto set_value(Ts_&&... ts) && noexcept -> decltype(std::declval().set_value( - std::forward(ts)...)) - { - auto r = std::move(*this); - // We first move the storage to a temporary variable so that - // this any_receiver is empty after this set_value. Doing - // std::move(storage.get()).set_value(...) would leave us with a - // non-empty any_receiver holding a moved-from receiver. - auto moved_storage = std::move(r.storage); + auto set_value( + Ts_&&... ts) && noexcept -> decltype(receiver->set_value(std::forward(ts)...)) + { try { - std::move(moved_storage.get()).set_value(std::forward(ts)...); + receiver->set_value(std::forward(ts)...); } catch (...) { - std::move(moved_storage.get()).set_error(std::current_exception()); + receiver->set_error(std::current_exception()); } } friend void tag_invoke(pika::execution::experimental::set_error_t, any_receiver&& r, std::exception_ptr ep) noexcept { - // We first move the storage to a temporary variable so that - // this any_receiver is empty after this set_error. Doing - // std::move(storage.get()).set_error(...) would leave us with a - // non-empty any_receiver holding a moved-from receiver. - auto moved_storage = std::move(r.storage); - std::move(moved_storage.get()).set_error(std::move(ep)); + r.receiver->set_error(std::move(ep)); } friend void tag_invoke( pika::execution::experimental::set_stopped_t, any_receiver&& r) noexcept { - // We first move the storage to a temporary variable so that - // this any_receiver is empty after this set_stopped. Doing - // std::move(storage.get()).set_stopped(...) would leave us with a - // non-empty any_receiver holding a moved-from receiver. - auto moved_storage = std::move(r.storage); - std::move(moved_storage.get()).set_stopped(); + r.receiver->set_stopped(); } friend constexpr pika::execution::experimental::empty_env tag_invoke( @@ -587,12 +494,95 @@ namespace pika::execution::experimental::detail { } }; + template + struct any_operation_state_holder_impl final : any_operation_state_holder_base + { + [[no_unique_address]] std::optional< + std::decay_t>>> operation_state; + + template + any_operation_state_holder_impl(Sender_&& sender, any_receiver&& receiver) + : operation_state(pika::detail::with_result_of([&]() mutable { + return pika::execution::experimental::connect( + std::forward(sender), std::move(receiver)); + })) + { + } + ~any_operation_state_holder_impl() noexcept = default; + + void start() & noexcept override + { + PIKA_ASSERT(operation_state.has_value()); + pika::execution::experimental::start(*operation_state); + } + }; + + class PIKA_EXPORT any_operation_state_holder + { + using base_type = detail::any_operation_state_holder_base; + template + using impl_type = detail::any_operation_state_holder_impl; + using storage_type = pika::detail::movable_sbo_storage; + + storage_type storage{}; + + public: + template + any_operation_state_holder(Sender&& sender, any_receiver&& receiver) + { + storage.template store>( + std::forward(sender), std::move(receiver)); + } + + ~any_operation_state_holder() noexcept = default; + any_operation_state_holder(any_operation_state_holder&&) = delete; + any_operation_state_holder(any_operation_state_holder const&) = delete; + any_operation_state_holder& operator=(any_operation_state_holder&&) = delete; + any_operation_state_holder& operator=(any_operation_state_holder const&) = delete; + + void start() & noexcept; + }; + + template + class any_operation_state + { + std::decay_t> receiver; + any_receiver_ref, Ts...> receiver_ref; + any_operation_state_holder op_state; + + public: + template + any_operation_state(Sender&& sender, Receiver_&& receiver) + : receiver(std::forward(receiver)) + , receiver_ref{&this->receiver} + , op_state{std::forward(sender).connect(any_receiver{&receiver_ref})} + { + } + + ~any_operation_state() noexcept = default; + any_operation_state(any_operation_state&&) = delete; + any_operation_state(any_operation_state const&) = delete; + any_operation_state& operator=(any_operation_state&&) = delete; + any_operation_state& operator=(any_operation_state const&) = delete; + + friend void tag_invoke( + pika::execution::experimental::start_t, any_operation_state& os) noexcept + { + os.op_state.start(); + } + }; + + [[noreturn]] PIKA_EXPORT void throw_bad_any_call( + char const* class_name, char const* function_name); +} // namespace pika::execution::experimental::detail + +namespace pika::execution::experimental::detail { template struct unique_any_sender_base { virtual ~unique_any_sender_base() noexcept = default; virtual void move_into(void* p) = 0; - virtual any_operation_state connect(any_receiver&& receiver) && = 0; + virtual any_operation_state_holder connect(any_receiver&& receiver) && = 0; virtual bool empty() const noexcept { return false; } }; @@ -602,7 +592,7 @@ namespace pika::execution::experimental::detail { virtual any_sender_base* clone() const = 0; virtual void clone_into(void* p) const = 0; using unique_any_sender_base::connect; - virtual any_operation_state connect(any_receiver&& receiver) const& = 0; + virtual any_operation_state_holder connect(any_receiver&& receiver) const& = 0; }; template @@ -612,7 +602,7 @@ namespace pika::execution::experimental::detail { bool empty() const noexcept override { return true; } - [[noreturn]] any_operation_state connect(any_receiver&&) && override + [[noreturn]] any_operation_state_holder connect(any_receiver&&) && override { throw_bad_any_call("unique_any_sender", "connect"); } @@ -629,12 +619,12 @@ namespace pika::execution::experimental::detail { bool empty() const noexcept override { return true; } - [[noreturn]] any_operation_state connect(any_receiver&&) const& override + [[noreturn]] any_operation_state_holder connect(any_receiver&&) const& override { throw_bad_any_call("any_sender", "connect"); } - [[noreturn]] any_operation_state connect(any_receiver&&) && override + [[noreturn]] any_operation_state_holder connect(any_receiver&&) && override { throw_bad_any_call("any_sender", "connect"); } @@ -657,9 +647,9 @@ namespace pika::execution::experimental::detail { void move_into(void* p) override { new (p) unique_any_sender_impl(std::move(sender)); } - any_operation_state connect(any_receiver&& receiver) && override + any_operation_state_holder connect(any_receiver&& receiver) && override { - return any_operation_state{std::move(sender), std::move(receiver)}; + return any_operation_state_holder{std::move(sender), std::move(receiver)}; } }; @@ -683,14 +673,14 @@ namespace pika::execution::experimental::detail { void clone_into(void* p) const override { new (p) any_sender_impl(sender); } - any_operation_state connect(any_receiver&& receiver) const& override + any_operation_state_holder connect(any_receiver&& receiver) const& override { - return any_operation_state{sender, std::move(receiver)}; + return any_operation_state_holder{sender, std::move(receiver)}; } - any_operation_state connect(any_receiver&& receiver) && override + any_operation_state_holder connect(any_receiver&& receiver) && override { - return any_operation_state{std::move(sender), std::move(receiver)}; + return any_operation_state_holder{std::move(sender), std::move(receiver)}; } }; } // namespace pika::execution::experimental::detail @@ -698,24 +688,20 @@ namespace pika::execution::experimental::detail { namespace pika::execution::experimental { #if !defined(PIKA_HAVE_CXX20_TRIVIAL_VIRTUAL_DESTRUCTOR) namespace detail { - // This helper only exists to make it possible to use - // any_(unique_)sender in global variables or in general static - // that may be created before main. When used as a base for - // any_(unique_)_sender, this ensures that the empty vtables for - // any_receiver and any_operation_state are created as the first thing - // when creating an any_(unique_)sender. The empty vtables for - // any_receiver and any_operation_state may otherwise be created much - // later (when the sender is connected and started), and thus destroyed - // before the any_(unique_)sender is destroyed. This would be + // This helper only exists to make it possible to use any_(unique_)sender in global + // variables or in general static that may be created before main. When used as a base for + // any_(unique_)_sender, this ensures that the empty vtables for any_operation_state are + // created as the first thing when creating an any_(unique_)sender. The empty vtables for + // any_operation_state may otherwise be created much later (when the sender is connected and + // started), and thus destroyed before the any_(unique_)sender is destroyed. This would be // problematic since the any_(unique_)sender can hold previously created - // any_receivers and any_operation_states indirectly. + // any_operation_states indirectly. template struct any_sender_static_empty_vtable_helper { any_sender_static_empty_vtable_helper() { - pika::detail::get_empty_vtable(); - pika::detail::get_empty_vtable>(); + pika::detail::get_empty_vtable(); } }; } // namespace detail @@ -791,24 +777,23 @@ namespace pika::execution::experimental { pika::execution::experimental::set_error_t(std::exception_ptr), pika::execution::experimental::set_stopped_t()>; - template - friend detail::any_operation_state - tag_invoke(pika::execution::experimental::connect_t, unique_any_sender&& s, R&& r) + template + friend detail::any_operation_state tag_invoke( + pika::execution::experimental::connect_t, unique_any_sender&& s, Receiver&& receiver) { // We first move the storage to a temporary variable so that this // any_sender is empty after this connect. Doing // std::move(storage.get()).connect(...) would leave us with a // non-empty any_sender holding a moved-from sender. auto moved_storage = std::move(s.storage); - return std::move(moved_storage.get()) - .connect(detail::any_receiver{std::forward(r)}); + return {std::move(moved_storage.get()), std::forward(receiver)}; } - template - friend detail::any_operation_state - tag_invoke(pika::execution::experimental::connect_t, unique_any_sender const&, R&&) + template + friend detail::any_operation_state + tag_invoke(pika::execution::experimental::connect_t, unique_any_sender const&, Receiver&&) { - static_assert(sizeof(R) == 0, + static_assert(sizeof(Receiver) == 0, "Are you missing a std::move? unique_any_sender is not copyable and thus not " "l-value connectable. Make sure you are passing a non-const r-value reference of " "the sender."); @@ -895,24 +880,23 @@ namespace pika::execution::experimental { pika::execution::experimental::set_error_t(std::exception_ptr), pika::execution::experimental::set_stopped_t()>; - template - friend detail::any_operation_state - tag_invoke(pika::execution::experimental::connect_t, any_sender const& s, R&& r) + template + friend detail::any_operation_state tag_invoke( + pika::execution::experimental::connect_t, any_sender const& s, Receiver&& receiver) { - return s.storage.get().connect(detail::any_receiver{std::forward(r)}); + return {s.storage.get(), std::forward(receiver)}; } - template - friend detail::any_operation_state - tag_invoke(pika::execution::experimental::connect_t, any_sender&& s, R&& r) + template + friend detail::any_operation_state + tag_invoke(pika::execution::experimental::connect_t, any_sender&& s, Receiver&& receiver) { // We first move the storage to a temporary variable so that this // any_sender is empty after this connect. Doing // std::move(storage.get()).connect(...) would leave us with a // non-empty any_sender holding a moved-from sender. auto moved_storage = std::move(s.storage); - return std::move(moved_storage.get()) - .connect(detail::any_receiver{std::forward(r)}); + return {std::move(moved_storage.get()), std::forward(receiver)}; } template diff --git a/libs/pika/execution_base/src/any_sender.cpp b/libs/pika/execution_base/src/any_sender.cpp index 0fce10a19b..1d6ac58adf 100644 --- a/libs/pika/execution_base/src/any_sender.cpp +++ b/libs/pika/execution_base/src/any_sender.cpp @@ -16,18 +16,15 @@ #include namespace pika::execution::experimental::detail { - void empty_any_operation_state::start() & noexcept + void empty_any_operation_state_holder_state::start() & noexcept { PIKA_THROW_EXCEPTION(pika::error::bad_function_call, "any_operation_state::start", "attempted to call start on empty any_operation_state"); } - bool empty_any_operation_state::empty() const noexcept { return true; } - - void tag_invoke(pika::execution::experimental::start_t, any_operation_state& os) noexcept - { - os.storage.get().start(); - } + bool any_operation_state_holder_base::empty() const noexcept { return false; } + bool empty_any_operation_state_holder_state::empty() const noexcept { return true; } + void any_operation_state_holder::start() & noexcept { storage.get().start(); } void throw_bad_any_call(char const* class_name, char const* function_name) {