-
Notifications
You must be signed in to change notification settings - Fork 13.3k
std: Force Instant::now()
to be monotonic
#56988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,13 @@ | |
|
||
#![stable(feature = "time", since = "1.3.0")] | ||
|
||
use cmp; | ||
use error::Error; | ||
use fmt; | ||
use ops::{Add, Sub, AddAssign, SubAssign}; | ||
use sys::time; | ||
use sys_common::FromInner; | ||
use sys_common::mutex::Mutex; | ||
|
||
#[stable(feature = "time", since = "1.3.0")] | ||
pub use core::time::Duration; | ||
|
@@ -150,7 +152,45 @@ impl Instant { | |
/// ``` | ||
#[stable(feature = "time2", since = "1.8.0")] | ||
pub fn now() -> Instant { | ||
Instant(time::Instant::now()) | ||
let os_now = time::Instant::now(); | ||
|
||
// And here we come upon a sad state of affairs. The whole point of | ||
// `Instant` is that it's monotonically increasing. We've found in the | ||
// wild, however, that it's not actually monotonically increasing for | ||
// one reason or another. These appear to be OS and hardware level bugs, | ||
// and there's not really a whole lot we can do about them. Here's a | ||
// taste of what we've found: | ||
// | ||
// * #48514 - OpenBSD, x86_64 | ||
// * #49281 - linux arm64 and s390x | ||
// * #51648 - windows, x86 | ||
// * #56560 - windows, x86_64, AWS | ||
// * #56612 - windows, x86, vm (?) | ||
// * #56940 - linux, arm64 | ||
// * https://bugzilla.mozilla.org/show_bug.cgi?id=1487778 - a similar | ||
// Firefox bug | ||
// | ||
// It simply seems that this it just happens so that a lot in the wild | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delete 'this'? |
||
// we're seeing panics across various platforms where consecutive calls | ||
// to `Instant::now`, such as via the `elapsed` function, are panicking | ||
// as they're going backwards. Placed here is a last-ditch effort to try | ||
// to fix things up. We keep a global "latest now" instance which is | ||
// returned instead of what the OS says if the OS goes backwards. | ||
// | ||
// To hopefully mitigate the impact of this though a few platforms are | ||
// whitelisted as "these at least haven't gone backwards yet". | ||
if time::Instant::actually_monotonic() { | ||
return Instant(os_now) | ||
} | ||
|
||
static LOCK: Mutex = Mutex::new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have dead-code elimination in MIR debug builds ? Otherwise LLVM-IR for this code will always be emitted independently of the result of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't, now, but |
||
static mut LAST_NOW: time::Instant = time::Instant::zero(); | ||
unsafe { | ||
let _lock = LOCK.lock(); | ||
let now = cmp::max(LAST_NOW, os_now); | ||
LAST_NOW = now; | ||
Instant(now) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A mutex seems a bit heavy-handed. Many uses of rdtsc (where available) are for minimal-overhead, thread-local timing of functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that a full-on mutex is quite a heavy hammer for this use case! I wasn't sure though how to best to minimize the cost here. The Windows documentation at least "strongly discourages" rdtsc for handling VM migration issues as well as some supposed hardware. If that's the case I think we probably want to avoid that? I figured it'd probably be best to start from a conservative position and we can always come in later as necessary and try to use atomics and/or different tricks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't mean to use I'm mostly concerned that thread-contention might unexpectedly hit people if they litter their code with instants because it used to be fast. I have no concrete examples, just experience with gratuitous use of timing functions that were fast on linux suddenly making an application slow on windows because it decided to not use rdtsc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I'm all for adding a more lightweight implementation, do you have one in mind? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had sizeof checks, some type punning and AtomicU128/U64 in mind. Beyond that it would be your standard read, check, CAS. similar to what you're now doing in the lock's critical section, except in a loop. The mutex would still be needed as fallback if the checks don't work out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok, that's what I thought, and yeah my worry about that is that it wouldn't solve the original problem of monotonic clocks going backwards, so I'm afraid we'd still end up a the solution proposed in this PR. We, as far as I know, don't have a great handle on how big the errors are. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have thought some more about optimization potential
It could approximately look like this: static mut LAST_NOW: AtomicU128 = 0.into();
let last_now = LAST_NOW.load(Relaxed); // insert type punning here
let os_now = time::Instant::now();
if likely(os_now > last_now) {
loop {
match LAST_NOW.compare_exchange_weak(last_now, os_now, Relaxed, Relaxed) {
Ok(_) => break,
Err(x) if x >= os_now => break, // some other thread is ahead of us, no need to update
_ => {}
}
return os_now;
}
return last_now It's a bit smaller hammer but still not the rubber kind. To soften it further we either need a I (don't) care about broken systems switch somewhere or use better platform detection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @the8472 yes that's all possible but 128-bit atomics are only available on a few platforms, so we can't use them generally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wouldn't it still make sense to try the cheaper thread-local version first and switch to a full lock if it does turn out to be insufficient? If we directly go to the lock, then we will not be able to determine whether a cheaper thread-local variant would also have been sufficient. @the8472's theory that this arises only due to migration between cores at least sounds very plausible, so I think it would be worthwhile to try this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nikic I think it's incorrect to avoid the full lock though? If the time is less than a thread-local version than you definitely have to acquire the lock, but even if it's greater than a thread local version you need to check the lock for the global one as well. Right now the bug primarily happens on one thread, but the documented guarantees of this API are that it works across all threads |
||
} | ||
|
||
/// Returns the amount of time elapsed from another instant to this one. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@edelsonh Can you confirm ppc/ppc64 has a reliable monotonic clock?