#ifndef _NTRTL_H
//
// Thread execution
//
/**
* The RtlDelayExecution routine suspends the current thread for the specified interval.
*
* \param Alertable If `TRUE`, the wait is alertable and may return early with STATUS_ALERTED or STATUS_USER_APC.
* \param DelayInterval A pointer to the interval to wait, in 100-nanosecond units.
* - A negative value specifies an interval relative to the current time.
* - A positive value specifies an absolute time.
* - A value of zero yields the remainder of the time slice to another ready thread.
* \return NTSTATUS Successful or errant status. STATUS_NO_YIELD_PERFORMED if DelayInterval is zero and no other thread was ready to run.
* \remarks When DelayInterval is zero and SMT delayed sleep is configured (Session Manager SmtDelayBaseYield/SmtFactorYield),
* repeated failed yields within SmtDelaySleepLoopWindowSize (after SmtDelaySpinCountThreshold calls) execute an increasing
* pause back-off, bounded by SmtDelayMaxYield, before the system call. State is tracked in TEB->SpinCallCount and
* TEB->LastSleepCounter; SpinCallCount is reset whenever the status is not STATUS_NO_YIELD_PERFORMED.
*
* At process start, LdrpInitializeSmtDelayedSleep reads five values from
* HKLM\\System\\CurrentControlSet\\Control\\Session Manager:
* - SmtDelaySleepLoopWindowSize: Only zero-length delays called within this many QPC ticks of the previous one count as a spin loop.
* - SmtDelaySpinCountThreshold: How many back-to-back failed yields before throttling starts.
* - SmtDelayBaseYield: Starting length of the pause.
* - SmtFactorYield: How much longer the pause gets with each further failed yield.
* - SmtDelayMaxYield: Upper limit on the pause.
*
* If both SmtDelayBaseYield and SmtFactorYield are 0 (absent), the feature is off and the function behaves like
* plain NtDelayExecution. The pause length is converted to a number of pause instructions using
* KUSER_SHARED_DATA->CyclesPerYield.
* \remarks Timing impact of the SMT yield backoff introduced on Windows 11 (derived from the RtlDelayExecution disassembly).
*
* Not affected:
* - Any non-zero delay (Sleep(n) with n > 0). The first test is *DelayInterval == 0;
* any other value goes straight to NtDelayExecution, so timing a Sleep(n) against QPC/RDTSC is unchanged.
* - Direct NtDelayExecution or NtYieldExecution calls. They go straight to the kernel and never reach the backoff.
* - Systems where SmtDelayBaseYield and SmtFactorYield are both zero/absent (plain pass-through).
*
* Affected (when the feature is enabled and only for zero-interval calls (Sleep(0), SwitchToThread, and wrappers that pass a zero interval):
* - Code that times Sleep(0)/SwitchToThread in a tight loop. After SmtDelaySpinCountThreshold consecutive failed
* yields inside the window, each call gets a growing run of pause instructions, up to SmtDelayMaxYield. Per-call
* latency rises and then levels off. Since the backoff only builds while yields keep failing, it mostly shows
* on an idle core with nothing else ready to run.
*
- Anti-debug, sandbox or VM checks that measure Sleep(0)/SwitchToThread timing may see inflated or uneven
* numbers and misread them as a debugger or emulator. On SMT hardware the pause bursts also change how much the
* sibling core runs, which can skew cross-core contention measurements.
*
* - Benchmarks that use Sleep(0) as a cheap yield inside the measured loop pick up extra latency on an idle core.
*
* - Spin-then-yield locks (.NET SpinWait/Thread.Yield, game-engine spin locks, lock-free retry loops). When the
* holder runs on another core the yield keeps failing, so the backoff builds; on release the waiter may be
* partway through a pause run of up to SmtDelayMaxYield, adding acquire latency that older Windows did not have.
*
* - Polling loops (audio, input, network) that poll with Sleep(0) get extra, machine-dependent jitter. The delay
* depends on TEB state (SpinCallCount, LastSleepCounter) left by earlier calls on the same thread.
*
* - Virtual machines: hypervisor pause-loop exiting (PLE) treats long pause bursts as a spinning vCPU and may
* deschedule it, so a short yield could become a latency spike far longer than the intended pause. Whether this
* happens depends on the PLE window/gap settings and CyclesPerYield.
*
* - CPU accounting and profiling: the pause loop runs in user mode, so it counts as user CPU time and thread
* cycle time attributed to ntdll!RtlDelayExecution, where older Windows showed mostly system-call/kernel time.
* Per-thread context-switch and system-call counts drop, so heuristics that use "many NtDelayExecution calls"
* as a busy-yield signal see different numbers. Thread CPU/cycle figures for processes stuck in yield loops
* will look higher on Windows 11 than on Windows 10 for the same behavior.
*
* - Priority starvation is unchanged: a zero-length delay still only yields to threads ready on the current
* processor, so a starved lower-priority lock holder is not helped, and each loop iteration now takes longer.
*
* - Inconsistent behavior: the settings are read once per process at ntdll initialization, so behavior differs
* across machines and only a process restart picks up a registry change. NtDelayExecution bypasses the backoff
* while Sleep/SwitchToThread do not, so two seemingly equivalent code paths can also time differently.
*
* Benefit: on SMT hardware, a thread repeatedly failing to yield used to issue a stream of system calls that took
* execution resources from its sibling core. The pause runs give those resources back and likely use less power
* than looping through the kernel.
*
* For a zero-length yield with no throttling (e.g. in a timing tool), call NtDelayExecution or
* NtYieldExecution directly instead of calling Sleep or RtlDelayExecution.
* \see NtDelayExecution
*/
NTSYSAPI
NTSTATUS
NTAPI
RtlDelayExecution(
_In_ BOOLEAN Alertable,
_In_ PLARGE_INTEGER DelayInterval
);
View code on GitHubNo description available.