From 22831af8105dfe070361e37652dca5dae85cf82c Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 27 Aug 2026 20:21:45 +0200 Subject: [PATCH] feat(example): add any-sender-size operation-state measurement Measures the operation state produced by connecting starts_on(sched, just(42)) through exec::any_sender / exec::any_receiver against the concrete operation state for the same pipeline, and counts the heap allocations made by connect and start through the replaceable operator new. Published so the numbers in P4251 have a checkable setup. Against stdexec 307b83c5, x86-64 Linux, Release: connect performs one allocation of 112-200 bytes in every case, exceeding the 64-byte small buffer; start allocates nothing; the concrete path allocates nothing. --- example/CMakeLists.txt | 1 + example/README.md | 6 ++ example/any-sender-size/CMakeLists.txt | 29 ++++++ example/any-sender-size/any_sender_size.cpp | 102 ++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 example/any-sender-size/CMakeLists.txt create mode 100644 example/any-sender-size/any_sender_size.cpp 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(); +}