quanta icon indicating copy to clipboard operation
quanta copied to clipboard

Can `Instant`s be `NonZeroU64`?

Open hawkw opened this issue 3 years ago • 4 comments

Currently, quanta::Instant is represented as a single u64 value. Will Instants ever be 0? If we're reasonably confident that Instants with the value 0 will never be generated, we may want to consider changing the internal representation to NonZeroU64. This will permit Option<Instant> to be niche optimized into a single 64-bit value.

My particular use case for this is that I'd like to be able to store an Instant in an AtomicU64, and some of those instants may be initially unset. With the current quanta API, I can implement this myself using Instant::as_u64, and using 0 as the unset value in my code.

However, my understanding is that, in quanta 1.0, the intention is to make Instant opaque and remove the as_u64 method. This means that it will be necessary to switch to crossbeam_util's AtomicCell type to store Instants atomically. When using AtomicCell with opaque Instant types, there's no way to initialize those cells to an "empty" value. I could use a specific "program start time" Instant as the zero value, but it would have to be passed around to a lot of places, making this code significantly more awkward.

Instead, it would be really nice to be able to use AtomicCell<Option<Instant>> and have it be lock-free on platforms with 64-bit atomics. This would require that Option<Instant> occupy a single 64-bit word, which is only possible if Instant is represented as NonZeroU64.

hawkw avatar Jan 14 '22 17:01 hawkw

Of course, if it's possible for quanta to ever generate Instants with the u64 value 0, this won't work, so we'd need to know for sure that this is the case. Otherwise, we'll need to find an alternative solution for my use case.

hawkw avatar Jan 14 '22 17:01 hawkw

Thinking through this one a little bit, and putting my thoughts down in this issue...

I think, practically speaking, there is an infinitesimally small chance of generating an Instant whose raw value is 0. While our reference time anchor may initially be zero -- unlikely, very unlikely, but technically possible -- and our TSC scaling factor could end up scaling a raw TSC read to zero... it's just... super unlikely. That said, it's technically possible: timespec in libc uses signed integers for both the second and nanosecond components. We might end up somewhere before zero, with the TSC addition bringing it up to zero.

There's also the technically-possible-but-highly-unlikely issue of integer overflow/wraparound as we scale up the raw TSC value and then add it to the reference time anchor, bringing it inline with the output you'd get from clock_gettime(CLOCK_MONOTONIC) directly.

While we could always construct an Instant such that we bitwise-AND the value with one, to ensure it's non-zero, that's an extra instruction for every single time we build an Instant, which is annoying. Maybe not practically meaningful, performance-wise, certainly compared to the other heavy-handed stuff that std::time is doing... but still not great.

tobz avatar Jan 15 '22 21:01 tobz

The other thing is that doing so would introduce a 1ns forward jump about half of the time, which isn't terrible in terms of accuracy, but it might make monotonicity invariants harder to achieve. Gotta think through that one...

tobz avatar Jan 15 '22 21:01 tobz

Just to close the mental loop around the bitwise-AND idea for my own sake: I think this would work.

Since we're only ever adding to the value, we won't be changing the numbers such that they warp backwards in time, which upholds the monotonicity invariant between two values of Instant. At worst, we may end up with two measurements -- T0 and T1 -- that, when uncorrected, are 1 nanosecond apart. If T0 was even, we would increment it by 1ns, which would make it equal to T1.

I'll whip up a simple PR to benchmark the change.

tobz avatar Jan 17 '22 23:01 tobz