diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index a767db1e1..2c9d96f3e 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(when-any-cancellation) if(BOOST_CAPY_BUILD_P2300_EXAMPLES) add_subdirectory(sender-bridge) add_subdirectory(awaitable-sender) + add_subdirectory(any-sender-size) endif() if(BOOST_CAPY_BUILD_CUDA_EXAMPLES) diff --git a/example/README.md b/example/README.md index 5db843c0f..7b26155e5 100644 --- a/example/README.md +++ b/example/README.md @@ -40,6 +40,12 @@ A complete echo server using Corosio for real network I/O. Requires Corosio. Data transformation through a pipeline of sources and sinks. +### any-sender-size/ + +Measures the `exec::any_sender` operation state and the heap allocation its +`connect` performs, against the concrete operation state for the same +pipeline. Requires stdexec (`BOOST_CAPY_BUILD_P2300_EXAMPLES=ON`). + ## Building ### CMake diff --git a/example/any-sender-size/CMakeLists.txt b/example/any-sender-size/CMakeLists.txt new file mode 100644 index 000000000..e27ebec25 --- /dev/null +++ b/example/any-sender-size/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# Copyright (c) 2026 Steve Gerbino +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/cppalliance/capy +# + +include(FetchContent) + +FetchContent_Declare( + stdexec + GIT_REPOSITORY https://github.com/NVIDIA/stdexec + GIT_TAG 307b83c5689ea7c2e5b31561cdc428697705333e + SYSTEM + FIND_PACKAGE_ARGS + NAMES stdexec +) +FetchContent_MakeAvailable(stdexec) + +add_executable(capy_example_any_sender_size any_sender_size.cpp) + +set_property(TARGET capy_example_any_sender_size PROPERTY FOLDER "examples") +target_compile_features(capy_example_any_sender_size PRIVATE cxx_std_23) +target_link_libraries(capy_example_any_sender_size STDEXEC::stdexec) + +add_test(NAME capy_example_any_sender_size + COMMAND capy_example_any_sender_size) diff --git a/example/any-sender-size/any_sender_size.cpp b/example/any-sender-size/any_sender_size.cpp new file mode 100644 index 000000000..cc0ec2bd7 --- /dev/null +++ b/example/any-sender-size/any_sender_size.cpp @@ -0,0 +1,102 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Measures the operation state produced by connecting a small sender +// pipeline through exec::any_sender / exec::any_receiver, against the +// concrete (non-erased) operation state for the same pipeline. Reports +// sizeof of each and the heap allocations made by connect and start, +// counted through the replaceable operator new. + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ex = stdexec; + +static std::size_t g_alloc_count = 0; +static std::size_t g_alloc_bytes = 0; + +void* operator new(std::size_t n) +{ + ++g_alloc_count; + g_alloc_bytes += n; + if (void* p = std::malloc(n)) + return p; + throw std::bad_alloc(); +} + +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, std::size_t) noexcept { std::free(p); } + +struct receiver +{ + using receiver_concept = ex::receiver_t; + std::atomic* done; + void finish() noexcept { done->store(true); done->notify_one(); } + void set_value(int) noexcept { finish(); } + void set_error(std::exception_ptr) noexcept { finish(); } + void set_stopped() noexcept { finish(); } + ex::env<> get_env() const noexcept { return {}; } +}; + +using any_sender = exec::any_sender< + exec::any_receiver>>; + +template +void measure(char const* label, Sender sender) +{ + using concrete_op = ex::connect_result_t; + using erased_op = ex::connect_result_t; + + std::atomic done{false}; + any_sender erased{sender}; + g_alloc_count = 0; + g_alloc_bytes = 0; + auto op = ex::connect(std::move(erased), receiver{&done}); + auto const connect_count = g_alloc_count; + auto const connect_bytes = g_alloc_bytes; + g_alloc_count = 0; + ex::start(op); + auto const start_count = g_alloc_count; + done.wait(false); + + done = false; + auto cop = ex::connect(std::move(sender), receiver{&done}); + g_alloc_count = 0; + ex::start(cop); + done.wait(false); + + std::printf("%s\n", label); + std::printf(" erased op state: %zu bytes, connect allocates %zu " + "(%zu bytes), start allocates %zu\n", sizeof(erased_op), + connect_count, connect_bytes, start_count); + std::printf(" concrete op state: %zu bytes, connect allocates 0, " + "start allocates %zu\n", sizeof(concrete_op), g_alloc_count); +} + +int main() +{ + std::printf("stdexec any_sender small buffer: 64 bytes\n"); + measure("starts_on(inline_scheduler, just(42))", + ex::starts_on(ex::inline_scheduler{}, ex::just(42))); + + exec::static_thread_pool pool(1); + measure("starts_on(static_thread_pool::scheduler, just(42))", + ex::starts_on(pool.get_scheduler(), ex::just(42))); + pool.request_stop(); +}