[native] Remove the libc++ dependencies from the CoreCLR host's timing - #12545
Open
simonrozsival wants to merge 14 commits into
Open
[native] Remove the libc++ dependencies from the CoreCLR host's timing#12545simonrozsival wants to merge 14 commits into
simonrozsival wants to merge 14 commits into
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/native/common/include/runtime-base/timing.hh — ❌ error: get_available_sequence() returns nullptr on malloc failure, but the current callers… |
What changed in this PR
This PR updates the native fast-timing implementation used by managed code to avoid handing out pointers into a growable std::vector buffer (which can reallocate and invalidate outstanding pointers). It replaces the sequence pool with a stable-address intrusive free list and updates the CoreCLR host to use a constant-lifetime Timing instance without heap allocation, supporting the broader effort to drop libc++ dependencies from the CoreCLR host.
Changes:
- Replace
Timing::sequence_pool(std::vector) with an intrusive free list of individuallymalloc’dmanaged_timing_sequencenodes. - Make the CoreCLR host timing singleton use a static inline instance (
_timing_instance) and point_timingat it when enabled (nonew Timing()).
| File | Description |
|---|---|
| src/native/common/include/runtime-base/timing.hh | Replaces vector-backed pool with free-list backed stable allocations for managed timing sequences. |
| src/native/clr/include/host/host.hh | Introduces a static inline Timing instance to avoid heap allocation when timing is enabled. |
| src/native/clr/host/host.cc | Switches timing initialization from new Timing() to using the static instance. |
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 27, 2026 19:29
a0a02ae to
e2be711
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 27, 2026 21:42
e2be711 to
8d1abce
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 07:54
f85389f to
149001c
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 08:47
149001c to
19ad6ac
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 08:56
19ad6ac to
2dc39cf
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 09:51
2dc39cf to
0789cfc
Compare
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 10:29
0789cfc to
27ee5c2
Compare
`Timing::sequence_pool` was a `std::vector<managed_timing_sequence>` that `get_available_sequence` scanned for a free entry, growing it with `emplace_back` when every entry was in use. Returning pointers into a vector's buffer is unsound. The constructor does `resize (16)`, which leaves capacity at exactly 16, so the seventeenth concurrent sequence reallocates the buffer -- and every pointer already handed out to managed code (held as an `IntPtr` across the `TimingLogger.Start`/`Stop` window) is left dangling. `monodroid_timing_stop` then writes `sequence->end` and `in_use = false` into freed memory, and the measurement is silently lost. Because those entries are never marked free again, the pool also grows on every subsequent call. Replace the vector with an intrusive free list. Entries are allocated individually with `malloc`, so they never move, and `release_sequence` pushes them back onto the list instead of freeing them. Nothing is ever freed, so no pointer can dangle; the total allocation is bounded by the peak number of concurrent sequences. Acquire and release are now O(1) rather than an O(n) scan under the lock. `in_use` is kept purely as a guard: a double release would otherwise push an entry onto the list twice and hand it to two callers at once. Today a double release is harmless, and it stays harmless. `Timing` is left with two constant-initialized POD members, so it no longer needs a constructor and can be a plain `static inline` instance in BSS, removing the `new Timing ()` as well. This only pays off on top of the `pthread_mutex_t` change: while `sequence_lock` was a `std::mutex` its non-trivial destructor forced `__cxa_atexit` registration behind a guard variable, which cost two more symbols than the `operator new` it saved. Real libc++ references in the CoreCLR archive drop from 40 to 38 (one `operator new`, one `__libcpp_verbose_abort` from the vector's length check). `__cxa_guard_*` stays at 8 and NativeAOT stays at 0. MonoVM also uses this class and gets the same fix without any change to `src/native/mono/`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`Host::_timing` was a pointer whose only job was to encode "fast timing is disabled" as `nullptr`. `FastTiming::enabled ()` already answers that question, so the pointer was redundant indirection over a static instance that always exists. Keep just the object, have `get_timing ()` return a reference, and gate both P/Invokes on `FastTiming::enabled ()`. This also closes a window where `enabled ()` was true but the pointer had not been assigned yet. Also null-check `get_available_sequence ()` in `monodroid_timing_start ()`: it can now return `nullptr` when `malloc` fails, which the previous vector-backed implementation never did. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The free list gave every sequence its own `malloc`, and threaded the recycling through a `next_free` pointer inside the sequence itself. That works, but a double release would put an entry on the list twice and hand it to two callers at once, so `release_sequence ()` had to guard against it. Allocate in chunks of 16 instead and go back to recycling through `in_use`, the way the original vector-backed code did. `get_available_sequence ()` scans the chunks for an unused entry and chains on a new chunk when it finds none. Chunks are never freed, so every address handed to managed code stays valid for the lifetime of the process, and a double release is just a redundant store. MonoVM shares `Timing` and dereferenced `get_available_sequence ()` without checking it, which was safe while the pool was a vector but is not now that allocation can fail. Add the missing check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Returning `nullptr` on allocation failure pushed the problem onto every caller, and both `monodroid_timing_start ()` implementations had to grow a check they never needed while the pool was a `std::vector`. Abort instead, which is what the rest of the runtime does when it cannot allocate. `get_available_sequence ()` can no longer fail, so both checks go away again and `src/native/mono/` is untouched by this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The previous commits replaced the `std::vector` backing `Timing`'s sequence pool with chunks allocated by `calloc` and chained together. `FastTiming`'s `TimingEventChunk` is a structurally identical pool that was left using `new`/`delete`, so apply the same treatment to it. This does not change the `libc++` reference count on its own, because the same translation units still reference `operator new`/`operator delete` for the `std::string` that `TimingEvent::more_info` points to. Removing those strings is done in the next commit of the stack, and only then does the count actually drop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`FastTiming::open_sequences` was a `thread_local std::stack<TimingEvent*>`, which defaults to `std::deque` as its container. `std::deque` has both a non-trivial constructor and a non-trivial destructor, so every translation unit including `timing-internal.hh` emitted a guarded dynamic initializer plus a `__cxa_thread_atexit` registration for the thread-local instance. The stack only ever needs `push`, `top`, `pop` and `empty`, and its depth is bounded by how deeply the instrumented calls nest (currently 3) because every `start_event` is matched by exactly one `end_event` or `store_more_info`. Replace it with a fixed `TimingEvent*` array plus a depth counter, both of which are trivially constructible and destructible and therefore constant initialized. `open_sequences` is `thread_local`, so it is private to each thread and needs no locking - that remains true here, as no state is shared between threads. The depth counter is incremented even when the array is full, so a push past the bound only loses that one entry instead of misaligning the pairing of the events below it. Once the depth drops back within bounds the remaining entries are still correct. Removes all 4 `__cxa_thread_atexit` references and one `__libcpp_verbose_abort`, taking the CoreCLR host's libc++ references from 64 to 59. As a side effect, pushing a timing event no longer allocates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The fixed array capped the nesting depth of timing events, which is not a
limit the timing code should impose - any number of events may be open on a
thread at once. Replace it with a naive singly linked list used as a stack,
with one malloc'd node per open sequence:
struct OpenSequence
{
TimingEvent *event;
OpenSequence *next;
};
static inline thread_local OpenSequence *open_sequences = nullptr;
The head pointer is still a trivially destructible thread-local, so this keeps
the property that motivated the change: no guarded dynamic initializer and no
`__cxa_thread_atexit` registration.
Nodes are freed as they are popped rather than being recycled, so a thread
that balances its `start_event` and `end_event` calls leaves nothing behind
when it exits. That matters here because, unlike the process-wide timing
sequence pool, this list is per thread and threads come and go.
Allocation failure aborts, matching how the timing sequence chunks behave.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`FastTiming` kept two heap-allocated `std::string`s that the earlier pass over the timing code missed: the per-event `TimingEvent::more_info` and the output file name parsed out of the `debug.mono.timing` property. `more_info` becomes a plain NUL-terminated `char*`. It was always built from one or two `std::string_view`s whose total length is known up front, so a single `malloc` and one or two `memcpy`s replace the string entirely. When the allocation fails we simply drop the extra information instead of aborting - timing is a diagnostic facility and must not take the application down with it. The output file name comes from a system property, whose value is limited to `PROP_VALUE_MAX` (92) bytes, so it now lives in a fixed 128 byte buffer inside `FastTiming` rather than in a `std::unique_ptr<std::string>`. Keeping it inline also means the global `internal_timing` instance stays constant-initialized and needs no guard variable. Names that do not fit are rejected with a warning and the default is used. Together with the previous commit this removes the last `operator new` and `operator delete` references from `timing-internal.cc.o` and, as a side effect, all of them from `typemap.cc.o`, which had been inheriting them from the inlined `new TimingEventChunk` in `FastTiming::get_event`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`std::function` is a type-erasing wrapper which needs to store, copy and
destroy an arbitrary callable, and it pulls `<functional>` into every
translation unit that sees the declaration. Neither of the two uses in the
CoreCLR host needs any of that.
`FastTiming::dump` took its line writer as `std::function<void(std::string_view const&)>`
by value. Of its two callers one passes a captureless lambda and the other
captures a single `FILE*`, so a plain function pointer plus an opaque
`void *context` covers both:
using LineWriter = void (*) (void *context, std::string_view const& line);
`AssemblyStore::configure_from_payload` took a `const std::function<std::string()>&`
used only to produce a path for diagnostics. Its only caller wrapped a
`const char *` in a `std::string` just so that the callee could call
`c_str ()` on it again, and the callback is invoked unconditionally in the
success path, so this allocated a string on every startup. It now takes the
`const char *` directly.
This does not change the number of undefined libc++ references, since both
uses were fully inlined by the optimizer, but it removes the generated
machinery: `libnet-android.release.so` shrinks by 6,976 bytes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Both `dump` callers either write to a file or ignore the context entirely, so there is no need for the context to be `void*`. Typing it as `FILE*` removes the `static_cast` in the file line writer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The two line writers were captureless lambdas converted to function pointers at the call site. That conversion goes through a compiler generated static invoker, so making them plain functions in an anonymous namespace removes a level of indirection: `libnet-android.release.so` shrinks by a further 56 bytes. The remaining lambdas inside `dump` are called directly rather than converted to function pointers, so the optimizer already inlines them completely - replacing those measured 2 bytes *larger*, so they are left alone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Addresses review feedback: `configure_from_payload()` takes a raw `const char*` and every use of it goes through `optional_string ()`, so the header comment now says explicitly that passing `nullptr` is allowed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`FastTiming::get_time()` already read the clock with `clock_gettime()`; `std::chrono::steady_clock` was only used as the type tag of the `chrono::time_point` the result was wrapped in. Store the timestamps as a plain `uint64_t` nanosecond count instead and drop `<chrono>` from the four files that included it (it was entirely unused in mainthread-dso-loader.hh). All four places that formatted an interval repeated the same seconds/milliseconds/nanoseconds split, so they now share a `time_interval` helper. The split is reproduced exactly as `chrono::duration_cast` computed it, so the timing output is unchanged - this matters because the format after the first colon is parsed by our performance measuring utilities. Also read `CLOCK_MONOTONIC` rather than `CLOCK_MONOTONIC_RAW`, so that we keep using the same clock `steady_clock` was documented to use. The two differ only in that `CLOCK_MONOTONIC` is slewed by NTP, which is irrelevant at the granularity we measure. This does not remove any undefined libc++ symbols - `<chrono>` is header only - but it does shrink libnet-android.release.so by 80 bytes and removes one more libc++ header from the build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
…tals Addresses review feedback. Both fields are totals for the whole interval and both are printed, so `milliseconds` is not milliseconds-within-the-second. The output format is consumed by performance measuring utilities, so spell this out to keep a future change from "correcting" it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
simonrozsival
force-pushed
the
dev/simonrozsival/clr-timing-free-list
branch
from
August 28, 2026 12:06
27ee5c2 to
2b26b61
Compare
This was referenced Aug 28, 2026
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.

Part of #12533 (drop
libc++from the CoreCLR host). Stacked on top of #12541.Why
The CoreCLR host's timing support is the last part of the host that reaches into
libc++for memory management, in two places:Timing::sequence_poolis astd::vector<managed_timing_sequence>, which pulls inoperator newand__libcpp_verbose_abort(the vector's own length check).Host::_timingholds thatTimingin astd::shared_ptr, which pulls in the whole__shared_weak_countfamily — atomic refcounting, the type-erased deleter and the control block destructor — none of which we need for an object that is created once and lives until the process exits.Neither buys us anything here. The pool is a fixed-purpose allocator for a diagnostic feature, and the
shared_ptrnever shares: there is exactly one owner and no lifetime question to answer.The constraint
Sequences are handed to managed code as an
IntPtrand held across theTimingLogger.Start/Stopwindow, so every address handed out has to stay valid until the caller releases it. That is the requirement the container has to satisfy, and it rules out any contiguous growable array — reallocating to grow moves the elements, invalidating pointers already given out. Growability was never really the requirement; stable addresses are.The fix
Allocate in chunks of 16 and chain them together:
get_available_sequence ()scans the chunks for an entry that isn't in use and chains on a new one when it finds none. Chunks are never freed, so an address stays valid for the lifetime of the process, and entries are recycled throughin_useexactly as the vector version did. Memory is bounded by peak concurrent usage rather than by total calls.An intrusive free list would also work, and would be O(1) rather than a scan. It is deliberately not used: threading the recycling through the entries themselves makes a double release actively dangerous, because the entry lands on the list twice and is then handed to two callers at once, so
release_sequence ()would need a guard against it. Recycling throughin_usekeeps a double release a harmless redundant store, which is the behaviour today. This is diagnostic code and the scan is over at most a handful of entries, so there is nothing to gain by trading that away.A chunk that cannot be allocated aborts, as elsewhere in the runtime, so
get_available_sequence ()never fails and no caller needs a null check.Dropping the
shared_ptrWith the vector gone,
Timingis left with two constant-initialized members and needs no constructor, so it can be a plainstatic inlineinstance in BSS. That removes the allocation entirely, and with it theshared_ptrthat owned it.Host::_timingwas then a pointer whose only remaining job was to encode "fast timing is disabled" asnullptr— a nullable pointer standing in for a boolean thatFastTiming::enabled ()already owns. So the pointer is gone too andget_timing ()returns aTiming&; it cannot beconst&, asget_available_sequence ()andrelease_sequence ()both mutate. Both P/Invokes ininternal-pinvokes-clr.ccnow gate onFastTiming::enabled ()instead, which also removes an ordering assumption: previouslyenabled ()could in principle be true before_timinghad been assigned.Note this only pays off on top of #12541. While
sequence_lockwas astd::mutex, its non-trivial destructor forced__cxa_atexitregistration behind a guard variable, and makingTiminga static was a net regression — measured at the time as +2 refs, trading oneoperator newfor two__cxa_guard_*. Now that the lock is apthread_mutex_t, the whole object is constant-initialized and the guard pair disappears as well.Results
__cxa_guard_*The nine removed, from diffing the demangled undefined-symbol sets of the two archives:
std::__ndk1::__shared_weak_count::__release_weak()std::__ndk1::__shared_weak_count::~__shared_weak_count()std::__ndk1::__shared_weak_count::__get_deleter(std::type_info const&) constoperator new(unsigned long)std::__ndk1::__libcpp_verbose_abort(char const*, ...)__cxa_guard_acquire__cxa_guard_releaseNativeAOT stays at 0.
Verification
MonoVMsharesTimingand picks up the same change, with no edits undersrc/native/mono/Also:
FastTiming's event chunksFastTiming::TimingEventChunkis a structurally identical chunked pool to the one introduced above — a fixed array of entries plus anextpointer, chained together and handed out as references that must stay valid for the lifetime of the process:It was still allocated with
new/delete, so it gets the samecalloc/freetreatment for consistency. Zero-filling matchesTimingEvent's default member initializers.This does not move the reference count on its own, because the same translation units still reference
operator new/operator deletefor thestd::stringbehindTimingEvent::more_info. Those are removed further down in this PR, and only then does the count actually drop — the metric is a per-file cliff, so only removing the last use of a symbol in an object counts.Also:
FastTiming::open_sequencesFastTiming::open_sequencestracks the timing events which have been started but not yet ended on each thread:std::stackdefaults tostd::dequeas its underlying container, andstd::dequehas both a non-trivial constructor and a non-trivial destructor. For athread_local, that means every translation unit which includestiming-internal.hhemits a guarded dynamic initializer for it, plus a__cxa_thread_atexitregistration so the deque is destroyed when the thread exits.timing-internal.hhis included by both the CoreCLR and the MonoVM hosts.The stack is only ever used through
push,top,popandempty, so a naive singly linked list is enough:The head pointer is a plain pointer, so it is trivially destructible and constant initialized: no guard variable, and nothing to register with
__cxa_thread_atexit. The list itself is unbounded — any number of events may be open at once, exactly as before.Locking:
open_sequencesisthread_local, so the list is private to its thread and needs no lock. This change keeps it that way — no state moves into shared storage — so there is still nothing to synchronize.Lifetime: nodes are freed as they are popped rather than being recycled. That matters here because, unlike the process-wide timing sequence pool above, this list is per thread and threads come and go — recycling nodes would mean every thread that ever recorded a timing event left its nodes behind. A thread that balances its
start_eventandend_eventcalls now leaves nothing allocated when it exits. Allocation failure aborts, matching how the timing sequence chunks behave.This removes the
__cxa_thread_atexitcategory entirely (4 → 0).The
push/top/pop/emptysemantics were checked against a standalone harness with instrumentedmalloc/free, covering the empty, balanced-LIFO, interleaved, over-pop and 100,000-deep cases, asserting strict LIFO order and that every node is freed. 20/20 pass.Also: the two
std::strings the earlier timing pass missed#12513 removed the local strings from the timing code, but two heap-allocated ones survived in
FastTimingand were only spotted later while attributing the remaininglibc++references.TimingEvent::more_infostd::string *more_info = nullptr;It is always built from one or two
std::string_views whose combined length is known up front, so it becomes a plain NUL-terminatedchar*produced by a singlemallocplus one or twomemcpys. If the allocation fails we drop the extra information for that one event rather than aborting — timing is a diagnostic facility and must not take the application down with it.FastTiming::output_file_namestd::unique_ptr<std::string> output_file_name{};The name is parsed out of the
debug.mono.timingsystem property, whose entire value is capped atPROP_VALUE_MAX(92) bytes, so a fixed 128 byte buffer insideFastTimingis always large enough. Keeping it inline also keeps the globalinternal_timinginstance constant-initialized, so it needs no guard variable. A name that does not fit is rejected with a warning and the default is used.<memory>and<string>are no longer needed bytiming-internal.hhat all.Together with the
callocchange above, this clears the lastoperator new/operator deletereferences fromtiming-internal.cc.o(2 → 0) and, as a side effect, fromtypemap.cc.o(2 → 0) —typemap.cchad been inheriting them purely from the inlinednew TimingEventChunkinFastTiming::get_event.Also: replacing
std::functionwith function pointersstd::functionis a type-erasing wrapper: it has to be able to store, copy and destroy an arbitrary callable, and it pulls<functional>into every translation unit that sees the declaration. Neither of the two remaining uses in the CoreCLR host needs any of that.FastTiming::dumpThe line writer was taken by value:
Of its two callers,
dump_to_logcatpasses a captureless lambda anddump_to_filecaptures a singleFILE*. A plain function pointer plus an opaque context covers both:The context is typed as
FILE*rather thanvoid*, since the only caller that needs one writes to a file — that avoids a cast in the writer.dump_to_filepasses itsFILE*through instead of capturing it, anddumpstays out of line — no template, so no code duplication per callback type.The two writers are plain functions in an anonymous namespace rather than captureless lambdas. A lambda converted to a function pointer goes through a compiler generated static invoker, so using functions directly removes a level of indirection.
AssemblyStore::configure_from_payloadThe callback existed only to produce a store path for diagnostics, but the single caller was:
That wraps a
const char *in astd::stringpurely so that all three use sites can call.c_str ()on it again. One of those three is the success-pathlog_debugfat the end of the function, so the callback runs on every startup and this allocated a string every time.It now takes the
const char *directly and uses the existingoptional_string ()helper, which also makes it null-safe. The stale comment describing the callback was updated — it claimed the path was only used for invalid payloads, which was not true.Why the reference count does not move here
Both uses were fully inlined by the optimizer at
-O2, sostd::function's machinery was emitted into the objects rather than left as undefined references. What goes away is that generated machinery — 6,992 bytes come offlibnet-android.release.so(546,904 → 539,912 B), plus onestd::stringallocation per startup.Worth recording, since it is easy to misread as "no progress":
llvm-nm --undefined-onlylists each undefined symbol once per object file, so this metric counts (object, symbol) pairs rather than call sites. Measured per object across this change:.texthost.cc.oassembly-store.cc.oThe count only moves when the last use of a symbol in a given file goes away, so it behaves as a per-file cliff rather than a gradual measure.
The lambdas remaining inside
dumpare called directly instead of being converted to function pointers, so the optimizer already inlines them; replacing them with named functions measured 2 bytes larger, so they were left as they are.Also: dropping
<chrono>FastTiming::get_time()has always read the clock withclock_gettime()directly — the comment above it even says we do that to avoid calling into libc++:std::chrono::steady_clockwas then used only as the type tag of thechrono::time_pointwe wrapped the result in. So we were paying for<chrono>without using the clock it provides.Timestamps are now a plain
uint64_tnanosecond count, and<chrono>is removed from the four files that included it. Inmainthread-dso-loader.hhthe include was entirely unused.Sharing the interval formatting
Four places repeated the same seconds / milliseconds / nanoseconds-within-the-millisecond split, so they now share one helper:
The split reproduces
chrono::duration_castexactly, including the fact that the middle field is the total milliseconds rather than a remainder. The timing output is byte-for-byte unchanged, which matters because the format after the first colon is parsed by our performance measuring utilities.I verified this rather than assuming it: a standalone harness compared the old
chronocomputation againsttime_intervalover the nine interesting edge cases (0,999999,1000000,1000001,999999999,1000000000, …,INT64_MAX) plus 2,000,000 random values — 2,000,009 checked, 0 mismatches.CLOCK_MONOTONIC_RAW→CLOCK_MONOTONICWe now read
CLOCK_MONOTONIC, the clocksteady_clockis specified to use, instead ofCLOCK_MONOTONIC_RAW. The two differ only in thatCLOCK_MONOTONICis slewed by NTP, which is irrelevant at the granularity we measure.<chrono>is header-only, so this removes no undefined symbols; it takes a further 80 B offlibnet-android.release.soand removes one more libc++ header from the build, which is a prerequisite for eventually building without libc++ headers at all.Overall result
Across the whole PR, the CoreCLR undefined-libc++ reference count goes from 47 (at #12541) to 38 after the chunked pool, and the
__cxa_thread_atexitand timing-relatedoperator new/operator deletecategories are eliminated entirely. NativeAOT stays at 0.src/native/mono/is untouched throughout, though MonoVM sharesTimingand picks up the same changes.CoreCLR, MonoVM and NativeAOT all build clean.