Repository navigation
feat: add --deterministic-time to make observed time reproducible - #61
Draft
not-matthias wants to merge 5 commits into
Draft
not-matthias wants to merge 5 commits into
not-matthias wants to merge 5 commits into
Conversation
LibVEX_SetVirtualTSC installs a counter that the RDTSC, RDTSCP (amd64, x86) and CNTVCT_EL0 (arm64) dirty helpers return instead of reading the host counter, and a frequency that CNTFRQ_EL0 reports. RDTSCP reports TSC_AUX as 0 so the CPU number cannot leak either. This mirrors LibVEX_SetRandom32: the core decides whether to install it, and NULL keeps the host behaviour.
not-matthias
force-pushed
the
deterministic-time
branch
from
November 4, 2026 10:22
23c75f5 to
94e910e
Compare
Merging this PR will improve performance by 11.01%
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
With --deterministic-time=yes the core keeps a virtual clock that advances by 1 ns per executed guest instruction, so the same instruction stream always observes the same time, however long Valgrind takes to run it. Valgrind's own work never advances it. A core instrument2 pass counts the IMarks of each superblock and adds the count to VG_(vclock_ns) before every conditional exit and at the end of the block, so every path adds exactly the instructions it executed. It runs after the tool's instrumentation, so tool costs are unchanged and tools never see the counter accesses. The counter is also installed as VEX's virtual TSC, so RDTSC and CNTVCT_EL0 tick at 1 GHz. m_vclock.c also maps Linux clock ids onto the clock: the realtime clocks are a fixed epoch (2024-01-01T00:00:00Z) plus the virtual time, the monotonic, boottime and CPU-time clocks are the virtual time, and sleeps move the clock forward. The syscall wrappers use these in the next commit.
With --deterministic-time=yes, clock_gettime, gettimeofday and time return the virtual clock, and nanosleep and clock_nanosleep move it forward by the requested duration (or up to the absolute deadline) and return at once instead of blocking. Clock ids the virtual clock does not cover (dynamic and unknown ones) still reach the kernel, as do the kernel's EINVAL and EFAULT checks on the request. A completed sleep yields to other threads, as a real sleep would let them run. The vDSO is already disabled under Valgrind, so libc reaches these syscalls.
Checks the properties of the virtual clock that hold on every host and compiler: the realtime clocks start at the fixed epoch, the monotonic clocks agree with each other, sleeps advance the clock without waiting, the TSC ticks once per virtual nanosecond, identical work takes identical time, and argument errors still match the kernel. Absolute values are not pinned because the instruction count before main() depends on libc.
not-matthias
force-pushed
the
deterministic-time
branch
from
November 4, 2026 10:49
94e910e to
8e8a952
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.
TLDR: Runtimes make decisions from elapsed time: benchmark harnesses pick iteration and warmup counts, GCs and schedulers use time-based heuristics, and time-bounded loops run until a deadline. Under Callgrind the host clock runs 20 to 100 times faster relative to the program than natively, and varies between runs, so all of these become instruction-count noise.
--deterministic-time=yesderives every clock the client can read from the number of guest instructions executed, so the same instruction stream always observes the same time (Linux only;--deterministic-rngalready lists this as a limitation).Model
CLOCK_REALTIMEis a fixed epoch (2024-01-01T00:00:00Z) plus the virtual time; the monotonic, boottime and CPU-time clocks are the virtual time.Core
m_vclock.cholds the counter and a VEXinstrument2pass that counts IMarks and adds them toVG_(vclock_ns)before every conditional exit and at the end of each superblock, so every path adds exactly the instructions it executed. It runs after the tool's pass, so tool costs are unchanged and tools never see the counter accesses. Cost is three IR statements per SB exit.vg_core_instrument_pass.Syswrap
clock_gettime,gettimeofdayandtimeare completed in PRE from the virtual clock. Dynamic and unknown clock ids still reach the kernel.nanosleepandclock_nanosleepmove the clock forward by the requested duration (or up to the absolute deadline) and return at once;EINVAL/EFAULTchecks match the kernel. A completed sleep setsSfYieldAfterso other threads still get to run.VEX
LibVEX_SetVirtualTSC(ticks, hz)hook, mirroringLibVEX_SetRandom32.RDTSC/RDTSCP(amd64) andCNTVCT_EL0(arm64) return the virtual time in ns;CNTFRQ_EL0reports 1 GHz;RDTSCPreportsTSC_AUXas 0.Review notes
CODSPEED-CHANGELOG.md): timeouts on blocking calls (futex,poll,epoll_wait), interval timers,getrusage/times, andCPUID's TSC frequency. Doing timeouts right needs a discrete-event scheduler; I left it out until a workload needs it.callgrind/tests/deterministic_timechecks properties that hold on every host (epoch, clocks agree, sleeps skip ahead, TSC tracks the clock, identical work takes identical time, kernel errors preserved). The full callgrind suite passes withVALGRIND_OPTS=--deterministic-time=yeswith the same pre-existing failures as without.Irvariance across runs under the flag, non-zero without) are in valgrind-helpers: AvalancheHQ/valgrind-helpers#6 (stacked on AvalancheHQ/valgrind-helpers#3).Smoke (5 ms monotonic budget loop, then
rdtscdelta, three runs each):