Skip to content

Hot Path Windows QPC Conversion

github-actions[bot] edited this page Sep 14, 2026 · 3 revisions

Windows QPC Clock Conversion

Lifecycle: Performance - Implemented · Type: Exact arithmetic shortcut
State: Present in current main.

Status

Field State at reviewed main
Underlying optimization Implemented
Current control None; route is selected automatically at process startup
Planned control Issue #99: developer-only Auto / Generic reference
Planned apply boundary Restart/startup only
Maintained reference muldiv64(ticks, 1,000,000,000, frequency)
Evidence counters Planned low-frequency route/profile evidence; absent from reviewed main
Qualification Source arithmetic path live; same-binary policy comparison not implemented
Reviewed source 322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14
Primary history Windows clock optimization associated with issue #19; planned diagnostic #99

Summary

On Windows, xemu/QEMU reads QueryPerformanceCounter and converts counter ticks to nanoseconds. The generic conversion uses muldiv64, which preserves precision for arbitrary QPC frequencies but can compile into expensive wide multiply/divide work on an extremely hot path.

At startup, current main queries QueryPerformanceFrequency. If one second in nanoseconds is exactly divisible by that frequency, xemu stores an exact integer nanoseconds-per-tick scale. Every later get_clock() call performs one branch:

exact scale available -> ticks * scale
otherwise             -> muldiv64 reference

Issue #99 plans a restart-latched developer diagnostic that can force the maintained generic route even on exact-capable hosts. There is deliberately no “Force exact” mode for non-divisible frequencies.

Why It May Help

get_clock() participates in timers, polling, profiling, deadlines, main-loop work, and many device/renderer paths. A small per-call saving can matter because call frequency can be very high.

Potential saving:

clock reads on exact-capable host
× avoided generic muldiv64 / wide division cost

It helps only when the host QPC frequency admits a lossless integer scale and clock conversion is materially hot. It does not reduce the cost of QueryPerformanceCounter itself, callers' timer logic, or unrelated virtual clock computation.

Observed in xemu

The optimized branch is present in reviewed main. On common Windows systems with a 10-MHz QPC frequency, one tick maps exactly to 100 ns; the multiply and generic conversion are mathematically equal for the supported range.

The reviewed binary does not expose the proposed generic-reference control, requested/effective status, or route evidence. Therefore a same-executable Auto/Generic retail campaign cannot yet be performed through the planned contract. Source and focused arithmetic checks can establish correctness, but current end-to-end benefit remains host/workload-dependent.

External Precedent

Direct host timing guidance: Microsoft recommends QPC for high-resolution interval timing, states that QueryPerformanceFrequency is fixed at boot, and discusses converting counter values without losing precision: Acquiring high-resolution time stamps.

Direct host API: QueryPerformanceFrequency returns the fixed counts-per-second frequency used to interpret QPC values: QueryPerformanceFrequency.

Microsoft's guidance does not prescribe xemu's exact-multiply specialization. xemu's optimization is valid because it retains the generic path unless exact divisibility is proven.

Emulation Boundary

Host monotonic counter ticks
    ↓
xemu/QEMU host-time conversion
exact multiply OR generic muldiv64
    ↓
host nanoseconds used by emulator clocks/waits/profiling
    ↓
emulator scheduling and timing decisions

This is not the emulated Xbox CPU clock, PIT, RTC, or NV2A PTIMER. It is a host utility conversion that influences when emulator components believe host time has advanced.

Code and Ownership Map

Location Role Owner
util/qemu-timer-common.c::init_get_clock() Query QPC frequency once, derive exact scale, initialize clock_start Process constructor/startup
include/qemu/timer.h::get_clock() Hot inline QPC read and multiply/reference branch Any calling thread
clock_freq Immutable startup frequency Process-global read-only after init
clock_ns_per_tick Current active exact scale; zero selects generic Startup-written, hot-read
issue #99 planned policy Separates immutable capability from active startup route Not implemented at reviewed main
tests/unit/test-xemu-windows-qpc.py and planned arithmetic C tests Source/selection/equality coverage Test process

Actual Current Flow

flowchart TD
    subgraph Startup
        A[QueryPerformanceFrequency] --> B{Call succeeds and frequency valid?}
        B -->|No| C[Fatal clock calibration failure]
        B -->|Yes| D{1,000,000,000 mod frequency == 0?}
        D -->|Yes| E[clock_ns_per_tick = exact quotient]
        D -->|No| F[clock_ns_per_tick = 0]
        E --> G[clock_start = get_clock]
        F --> G
    end
    subgraph EveryClockRead
        H[QueryPerformanceCounter] --> I{clock_ns_per_tick nonzero?}
        I -->|Yes| J[Return ticks * exact scale]
        I -->|No| K[Return muldiv64 ticks, 1e9, frequency]
    end
Loading

The hot path contains no general settings lookup, mutex, allocation, logging, or additional policy object.

Maintained Reference Flow

Planned Generic reference mode should select at startup:

flowchart TD
    A[QPC frequency and exact capability discovered] --> B[Requested startup policy = Generic reference]
    B --> C[Active clock_ns_per_tick = 0]
    C --> D[Every get_clock reads QPC]
    D --> E[muldiv64 conversion]
    E --> F[Same nanosecond result on exact-capable host]
Loading

For a non-divisible frequency, Auto already uses this route. Generic reference changes nothing and should report that clearly.

Eligibility and Protected Invariants

Invariant Why required Failure if weakened
Frequency > 0 and query succeeds Conversion divisor/source must be valid Divide by zero/invalid time
Exact route only when 1e9 % freq == 0 Integer scale must be lossless Systematic drift/truncation
Generic muldiv64 retained Arbitrary frequencies need precise ratio Wrong nanoseconds
Scale/frequency immutable after startup Hot callers are unsynchronized Data race/time discontinuity
No live policy lookup in get_clock() Extra atomic/config read contaminates optimization Reduced benefit/new contention
Multiply stays in supported integer range Result must not wrap intended clock domain Time reversal/large jump
Both routes monotonic Callers assume forward host time Timer/order failure
clock_start coherent with selected initial route Relative clock origin must match conversion Offset discontinuity
Tracepoints avoided in low-level clock helper Tracing may recursively read time Infinite recursion/reentrancy

There is no safe user option to force a truncated exact scale.

State, Lifetime, Invalidation, and Fallback

Current main stores clock_freq and clock_ns_per_tick during the process constructor. They are then read by many threads for the process lifetime.

Issue #99 proposes separating:

clock_exact_ns_per_tick  immutable mathematical capability
clock_ns_per_tick        active startup-selected route
requested policy         saved value, possibly restart pending

A live UI change must update only the saved/requested state and display Restart required. The active scale remains unchanged until the next process start.

Fallback is inherent: if exact capability is unavailable, Auto leaves the active scale zero and every call uses muldiv64.

Expected Profile Signature

On an exact-capable host:

same clock-read count and nanosecond values
Auto:
    exact multiply route count == reads
Generic:
    muldiv64 route count == reads
    higher clock conversion CPU expected

The benefit is likely visible in aggregate CPU samples rather than one frame marker because get_clock() is spread across many callers.

Disproof/limitation:

  • QPC frequency is non-divisible, so both modes are generic;
  • QueryPerformanceCounter dominates conversion;
  • clock reads are not hot in the selected workload;
  • compiler already optimizes the generic ratio effectively on a given toolchain;
  • added diagnostic instrumentation costs more than the route difference.

Use sampling or startup-latched low-frequency aggregate counts, not per-call formatted logging.

Counters and Evidence

Because this path is extremely hot, planned evidence should avoid another branch/atomic increment per call in normal production. Options include build-time diagnostics, thread-local sampled counters, or startup route metadata plus external profiles.

Evidence Unit Meaning
QPC frequency Hz Host capability input
exact scale capability ns/tick Zero when divisibility fails
requested/effective route enum Startup policy and active branch
restart pending bool Saved request differs from active route
sampled clock-read count samples/calls Route frequency, diagnostic only
get_clock / muldiv64 sampled weight ms/% External profile impact
differential arithmetic cases values Exact versus generic equality
monotonicity violations count Must remain zero

Evidence must record compiler, optimization/LTO, executable hash, and matching DWARF because generated arithmetic cost is build-dependent.

Risks and Known Bugs

  • Assuming common 10-MHz QPC frequency on every host is incorrect.
  • Integer division without remainder proof introduces drift.
  • Large tick multiplication can overflow if supported-domain assumptions are wrong.
  • Writing active scale live can produce discontinuity/races across threads.
  • Adding a new hot atomic policy read can erase the optimization being tested.
  • Profiling with a Debug build changes timing and generated arithmetic; use optimized Release plus matching DWARF.
  • A lower clock-helper sample share may reflect fewer clock calls or different guest progress rather than cheaper conversion.
  • Time values must match; this is not an acceptable approximate shortcut.

See issue #99, issue #19, Debugging Lag & Hot Paths, and Debugging Tools & Commands.

Qualification

  1. Frequencies: 10 MHz, 1 MHz, 1 GHz, 3,579,545 Hz, zero, and >1-GHz non-divisible cases.
  2. Exact-capable equality versus muldiv64 for zero/one tick, second boundaries, fixed pseudorandom values, and supported high range.
  3. Non-divisible frequencies never select exact route.
  4. Monotonic increasing sequence and overflow boundary analysis.
  5. clock_start/relative-clock coherence.
  6. Planned Auto/Generic config save/reload and restart-pending behavior.
  7. Startup apply before worker threads; no live active-scale change.
  8. Release/O2/LTO disassembly verifying hot path remains QPC + one branch + multiply/reference.
  9. Same executable separate-start ABBA/BAAB on exact-capable host.
  10. External sampling of clock/muldiv hot paths, host CPU, guest progress, p95/p99/max.
  11. GL and Vulkan representative workloads and full applicable XISO correctness.
  12. Non-Windows build/source control proving no accidental change.

The Generic mode belongs in developer diagnostics. Auto remains safe only because it selects multiplication after a mathematical proof of exact equivalence.

Clone this wiki locally