Arm64: opt-in low-power spin-wait using FEAT_WFxT (WFET)#130362
Draft
EgorBo wants to merge 5 commits into
Draft
Conversation
Add an opt-in, disabled-by-default low-power spin-wait for Arm64 based on the FEAT_WFxT WFET (Wait For Event with Timeout) instruction. When the DOTNET_ThreadWfetSpinWait knob is set to 1 and the hardware supports FEAT_WFxT together with FEAT_ECV (for the self-synchronized CNTVCTSS_EL0 counter read), YieldProcessorNormalized and its variants issue a single low-power WFET timed wait for the equivalent duration instead of a busy YieldProcessor loop. CPU utilization stays about the same (the thread remains scheduled) but energy usage while spinning drops substantially. Details: * minipal detects FEAT_WFxT+FEAT_ECV (Linux HWCAP2_WFXT/HWCAP2_ECV, macOS sysctl hw.optional.arm.FEAT_WFxT/FEAT_ECV) as ARM64IntrinsicConstants_Wfxt, and provides minipal_wfet_wait_ns(). The WFET/CNTVCTSS/CNTFRQ/SB instructions are emitted via .inst so the sources build without an Armv8.7 assembler. The wait is scaled by CNTFRQ_EL0 (fixed 1GHz from Armv8.6 in the common case). * The runtime enables the flag in EEJitManager::SetCpuInfo() when the knob is set and the feature is present. Non-Windows Arm64 only (WFET is emitted via GNU inline asm). * When the knob is off (the default), behavior is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match Arm's recommended WFET delay sequence (nano_delay_v87 from the "Multi-threaded applications" developer.arm.com blog) by comparing the elapsed ticks against the requested duration (current - start < ticks) instead of an absolute (current < target) comparison. This makes the wait loop robust to system counter roll-over. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
|
Tagging subscribers to this area: @agocke |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in, low-power Arm64 spin-wait path that uses FEAT_WFxT (WFET) when supported (and gated on FEAT_ECV for CNTVCTSS_EL0 timing). The runtime enables it via a new config knob and uses the minipal implementation from YieldProcessorNormalized* hot paths.
Changes:
- Add a new runtime-only Arm64 CPU feature flag (
ARM64IntrinsicConstants_Wfxt) and detect it via Linux HWCAP2 / Apple sysctl. - Introduce
minipal_wfet_wait_ns(...)+ a runtime opt-in flag (g_minipalWfetSpinWaitEnabled) for non-Windows Arm64. - Add the
ThreadWfetSpinWaitconfig knob and wire it up in CoreCLR startup, then consume it inYieldProcessorNormalized*.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/minipal/cpufeatures.h | Adds WFxT feature bit and declares WFET wait API + opt-in flag for non-Windows Arm64. |
| src/native/minipal/cpufeatures.c | Detects FEAT_WFxT+ECV and implements the WFET timed-wait helper and opt-in flag storage. |
| src/coreclr/vm/codeman.cpp | Enables WFET spin-wait at startup when supported and when ThreadWfetSpinWait is set. |
| src/coreclr/tools/Common/Compiler/HardwareIntrinsicHelpers.cs | Reserves the WFxT bit to keep the bit layout synced with minipal (not mapped to an ISA). |
| src/coreclr/inc/yieldprocessornormalized.h | Routes normalized yielding through the WFET wait when enabled. |
| src/coreclr/inc/clrconfigvalues.h | Adds the ThreadWfetSpinWait opt-in config knob. |
* minipal_wfet_wait_ns: read CNTFRQ_EL0 inline instead of caching it in a racy function-local static. CNTFRQ_EL0 is a constant register, so the per-call mrs is cheap and removes the data race on the shared cache. * Use a 128-bit intermediate for the ns->ticks scaling so the multiply cannot overflow when CNTFRQ_EL0 is not 1GHz. * TryYieldProcessorWithWfet: take 'unsigned int' instead of 'size_t' so that 'normalizedYields * TargetNsPerNormalizedYield' cannot overflow the uint64_t result. All call sites already pass unsigned int. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Open
3 tasks
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment on lines
+86
to
+88
| #ifndef HWCAP2_ECV | ||
| #define HWCAP2_ECV (1 << 19) | ||
| #endif |
This was referenced Jul 9, 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.
Closes #118933
Problem: Unlike x64 with its
pauseinstruction, Arm64 has had to spin on a no-op instruction in a loop, loading a CPU core to 100% (on most, if not all, hardware theyieldinstruction is just an alias fornop). We even had to reduce the maximum spin-wait duration in .NET 9/10 to address the higher CPU usage some of our customers observed when they moved from x64 to Arm64.Armv8.7 introduced
WFxT(Wait For Event/Interrupt with Timeout), which can be used for this purpose. It is supported by Neoverse N3/V3 (Cobalt 200/Graviton5) and Apple M4+. Our CI has M4 in the Arm64 Helix queues.I've disabled it by default (it will be enabled for CI only) so that we can first ask some 1P customers to check whether they observe any improvement/regression in any metric first.
Performance / benchmarks
Since the whole point is to reduce power usage while spinning, I validated the impact locally on an Apple M5 (which supports
FEAT_WFxT), measuring per-task energy via the macOStask_info/TASK_POWER_INFO_V2accounting. In every case the wall-clock time and CPU utilization are essentially unchanged — the spinning thread stays scheduled — but the energy it burns while spinning drops substantially:Thread.SpinWait: roughly 5-6x lower energy (e.g. ~57 J → ~10 J over the same interval), with identical wall/CPU.SpinWait, where worker threads spin on the pool's semaphore instead of parking (via a raisedDOTNET_ThreadPool_UnfairSemaphoreSpinLimit): roughly 2x lower energy (~37 J → ~16 J), with CPU held constant.One caveat worth stating explicitly: WFET reduces the power of spinning, not scheduler occupancy — the thread still holds its core for its time slice, it just idles the pipeline at low power. On server Arm (Neoverse), WFE/WFET can additionally release SMT/execution resources, so the tradeoff may be even more favorable there. These are illustrative local Apple M5 numbers; proper validation should be done on N3/V3-class server hardware.
Increasing the spin-wait duration
Because spinning becomes low-power, this also reverses the latency/energy tradeoff that forced us to reduce the maximum spin-wait duration in the first place (see the Problem section). With WFET we can afford to spin longer — keeping threads ready and avoiding the kernel wake round-trip (~2-10 µs), which lowers dispatch/lock latency — without paying the usual CPU/energy cost. Actually retuning those limits would be a separate, benchmark-driven follow-up (and, per the caveat above, still bounded under oversubscription).
References
Note
The grammar pass and the Performance/Increasing-spin-wait notes above were drafted with GitHub Copilot.