Split future<T> into future_base + future<T> - #107
Merged
Conversation
kammce
force-pushed
the
split-future-base
branch
from
August 25, 2026 00:48
f693b14 to
577fe1e
Compare
Moves everything in future<T> that never depended on T - the completion-state tag, the pending coroutine handle, exception_ptr storage, cancel-on-drop, cross-context await bookkeeping, and await_resume()'s exception/cancelled/terminate cold path - into a non-template future_base, mirroring the existing promise_base/promise<T> split. Only T's own value storage and the "extract T" fast path stay templated. Collapses what were 9+ distinct per-T instantiations of this machinery down to one shared copy each. Measured on a coroutine-heavy USB demo: 74,893 -> 66,813 bytes of .text, on top of the already-shipped linker ICF change. promise_return_base<T>/<void> also gain `export`, since their out-of-line members (needed once future<T> is a complete type) are now reached through the exported promise<T> template from other translation units and need real cross-TU linkage, not just module-internal visibility. benchmarks/benchmark.cpp's variant-return-overhead microbenchmark used the internal future_state<T> alias directly; decoupled it with a locally -defined equivalent variant, since it was never actually testing future<T>'s specific representation. Fixes #106
kammce
force-pushed
the
split-future-base
branch
from
August 25, 2026 02:40
577fe1e to
f11607b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
async::future<T>into a non-templatefuture_base(completion-state tag, pending coroutine handle,exception_ptrstorage) and a thinfuture<T>derived layer (onlyT's own value storage) - mirroring the existingpromise_base/promise<T>split in this same file.~future_base(),await_suspend(),await_resume()'s exception/cancelled/terminate cold path,done(),is_cancelled(),has_value(),resume(), andcancel()all become single, non-template, shared functions instead of being re-instantiated once perT.future<T>::cancelledmoves tofuture_base::cancelled(inherited, sofuture<T>::cancelledstill resolves for existing call sites) - it was already fullyT-independent in content, just needlessly duplicated perTfor a vtable +what().await_suspend()also drops itstemplate<typename U>parameter (the calling coroutine's promise type) - the body only ever calls.promise().get_context(), which lives onpromise_base, soUwas never load-bearing. This explains why so many distinctawait_suspend<...>symbols existed before ICF: it was aT x Ucross product, not just aTaxis.promise_return_base<T>/promise_return_base<void>gainexport: their out-of-line members (which needfuture<T>to be a complete type, so can't be defined in-class) are reached through the exportedpromise<T>template from other translation units, and need real cross-TU linkage rather than module-internal visibility. Without this, consumers instantiatingpromise<void>locally hitundefined symbol: promise_return_base<void>::return_void()at link time.benchmarks/benchmark.cpp's virtual-call-returning-a-variant microbenchmark used the internalfuture_state<T>alias directly; decoupled it with a locally-defined equivalent variant, since it was never actually testingfuture<T>'s specific representation.Motivation
Investigating ROM usage on a coroutine-heavy USB CDC demo (libhal-arm-mcu v2, stm32f103zg, Clang 20, MinSizeRel) found
future<T>'s per-await machinery fully duplicated per distinctT, even though most of it - "am I still pending", "rethrow the exception", "throw cancelled" - never touchesT. Linker ICF (libhal-cmake-util5.0.11) already collapses byte-identical duplicates (e.g. severalstrong_ptr<X>futures with the same layout regardless ofX), but can't help with genuinely different-sizedTs. On that demo, 9 distinct~future<T>()bodies and ~8 distinctawait_resume()bodies existed forvoid,bool,unsigned int,unsigned long long, and others - this PR collapses that down to one shared implementation each.Fixes #106
Test plan
basics,cancel,blocked_by,mutex,proxy,sync_wait,context_listener,simple_scheduler,clock_adapter,run_until_done,async_stacking,cross_context_await) viaconan build . -pr:a hal/tc/llvm-20-test_cancelandtest_cross_context_awaitspecifically exercise the cancel-on-drop and cross-context-await logic that moved intofuture_base.WarningsAsErrors: "*").stm32f103zgand rebuiltlibhal(v5 core),libhal-util,libhal-usb, andlibhal-arm-mcu'susb/usb_cdc_raw/can/adc/uart/blinker/blankdemos from source against it. All link and produce correct binaries.usbdemo (enumerator-based USB CDC-ACM):.textwent from 74,893 -> 66,813 bytes, on top of the already-shipped ICF change. Combined with a follow-up one-linedouble->floatfix in an unrelated demo clock adapter, the full demo now fits in a 64KB-flash target (stm32f103c8) that it did not fit in before this investigation started.