-
-
Notifications
You must be signed in to change notification settings - Fork 19
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
Proposal for new approach with nanoseconds integers #107
Proposal for new approach with nanoseconds integers #107
Conversation
Thanks for the pr! If I read the benchmarks correctly, using nanos and centuries is about 3 times faster than two floats for the Also, for the record, the conversion was about whether an FPU was available on embedded platforms, right? And that, typically, an FPU is slower than a APU. Was there another reason? |
Just updated the PR. Indeed, going to f64 is slower but all other operations in the end should be faster. For now, I use a 128-bit type as intermediary for the addition operation, which should not be needed when optimizing further as we could switch to a Apart for performance, advantages are compatibility with platforms without FPU and guaranteed nanosecond-precision anywhere in the possible Duration range (implementations using floating-points have variable time precision across the range, with very low precision at the extremes). |
f3b75bc
to
bb28a35
Compare
Cleaned-up the branch a bit, almost all tests are passing now. Still need some work on the printing side, and it looks like there might be some nasty rounding errors I need to track down, but I'm positive this gets finalized soon™ |
7604317
to
1529409
Compare
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.
Very good job! I think we (you) should change centuries to a smaller representation because we don't need lots of bits there. Also, avoid the overflows with the ::from()
call instead of coercing the conversion where possible.
src/duration.rs
Outdated
pub struct Duration(TwoFloat); | ||
pub struct Duration { | ||
ns : u64, // 1 century is about 3.1e18 ns, and max value of u64 is about 1e19.26 | ||
centuries : i64 // +- 9.22e18 centuries is the possible range for a Duration |
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.
Yes, I think that an i16 would be sufficient: that's still 256 centuries before J1900 and after J1900, so about 38% to the oldest cave paintings in the world (-660 centuries ago).
Also, in doing so, we're moving from 2x64 bits to 64+16=80 bits, thereby saving 48 bits.
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.
Agreed, will edit
src/duration.rs
Outdated
Self { | ||
0: days * TwoFloat::from(SECONDS_PER_DAY_U), | ||
} | ||
pub fn new(ns : u64, centuries : i64) -> Self { |
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.
pub fn new(ns : u64, centuries : i64) -> Self { | |
pub fn new(centuries : i64, ns : u64) -> Self { |
It makes more sense to me to have the larger representation first.
src/duration.rs
Outdated
Self { | ||
0: hours * TwoFloat::from(SECONDS_PER_HOUR_U), | ||
|
||
pub fn total_ns(self) -> i128 { |
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.
From my rough math, it seems like an i128 can represent +/- 5.8 centuries. So I think there should be several functions instead of just total_ns
, and I think that total_ns
should have a warning: "Will panic if more than 5.8 centuries away from J1900". Then, add a try_total_ns
that can't panic (returns a Result<i128, Error>
of enum Overflow
or something like that). And a third function called try_ns_from(baseline_century: i16)
that shifts the reference century.
Question: should the base century be J2000? All astro stuff uses J2000, only Network Time Protocol uses J1900, but all of our applications so far are for astro...
>>> (2**64 / (1e9)) / (86400.*365.25*100)
5.845420460906264
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.
Maybe the switch to J2000 should be done in another PR to merge this one in faster.
src/duration.rs
Outdated
|
||
fn normalize(&mut self) { | ||
if self.ns > NS_PER_CENTURY_U as u64 { | ||
let carry = self.ns / NS_PER_CENTURY_U as u64; |
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.
I think that where possible, the code should be u64::from(...)
because that forces the compiler to check that the operation is lossless and can't panic. This would give me some reassurance in the conversions.
src/duration.rs
Outdated
let centuries = (value.div_euclid(century_divider as f64)) as i64; | ||
value = value.rem_euclid(century_divider as f64); | ||
|
||
// Risks : Overflow, loss of precision, unexpected roundings |
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.
Yeah, that's fair. We should add tests for that and try to de-risk it, maybe by doing lots of conversions manually or the u64::from method.
src/duration.rs
Outdated
assert!((sum.in_unit_f64(TimeUnit::Minute) + 35.0).abs() < EPSILON); | ||
} | ||
|
||
#[test] | ||
fn duration_print() { | ||
// Check printing adds precision | ||
assert_eq!( | ||
format!("{}", TimeUnit::Day * 10.0 + TimeUnit::Hour * 5), | ||
"10 days 5 h 0 min 0 s" | ||
format!("{}", TimeUnit::Day * 10.0 + TimeUnit::Hour * 5).trim(), |
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.
Why trim()
?
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.
Because my Display implementation adds a space at the end of the string and I thought I'd rather trim() the tests than add complexity to the impl just for a single space
src/duration.rs
Outdated
"-5 h 0 min 0 s 256 ms 3.5 ns" | ||
TimeUnit::Hour * -5 + TimeUnit::Millisecond * -256 + dbg!(TimeUnit::Nanosecond * -3) | ||
).trim(), | ||
"-5 h 0 min 0 s 256 ms 0 us 3 ns" |
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.
Nice! This is better I think
src/duration.rs
Outdated
format!("{}", now), | ||
"14889 days 23 h 47 min 34 s 0 ms 203.125 ns" | ||
format!("{}", now).trim(), | ||
"40 years 289 days 23 h 47 min 34 s 0 ms 0 us 203 ns" |
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.
Hmm, why is this test different? Actually, shouldn't the original test ending with 123ns actually display 203 nanoseconds as you do? Seems like a bug in the previous version
src/duration.rs
Outdated
format!("{}", arbitrary), | ||
"14889 days 23 h 47 min 34 s 0 ms 123 ns" | ||
format!("{}", arbitrary).trim(), | ||
"40 years 289 days 23 h 47 min 34 s 0 ms 0 us 123 ns" |
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.
Are you sure this test is correct? Can we add more years
tests because I'm just worried we'll get the conversion wrong.
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.
I assumed 1 year = 365 days, as this is only used for Display. It's easy to rollback the years
part of the decomposition if you're not comfortable with it, I just thought I'd roll with it as it was basically free in the new Display impl. We could indeed add more years
tests though
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.
I think that the IETF or someone defines a year as 365.25 days (since one century is 36525 days). But the problem is that it's an approximate definition, and that's why lots of implementations don't allow for year based computations: it's confusing.
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.
Understood, it makes sense, I will remove this part of the decomposition then
Okay, I did a few changes to this branch. It doesn't work yet, but there's progress: I unified the
|
Just posting some temporary benchmarks from this PR after I started working on it last week. Summary:
With nanosecond precision
With TwoFloatNote: this was executed after the nanosecond precision so the "performance" increase/decrease metrics are compared to the nanosecond branch
|
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Remove lots of impl_ops for Duration because rustc isn't happy Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Crap... just implement a bunch of code fixes and now the performance is quite bad. Edit: I think the issue is the creation of a |
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Ha! Switching the creation of a
|
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Final benchmarks
|
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Signed-off-by: Christopher Rabotin <christopher.rabotin@gmail.com>
Did some light refactoring and benchmarked (will be deleted before merging). What are your thoughts about this approach ?
Note : this currently relies on bigint_helper_methods (rust-lang/rust#85532) so compiles on Nightly only