Skip to content

Commit 6bd65a1

Browse files
authored
Improve efficiency of outer until() algorithm by starting off years/months calculations at a minimum bound (#7682)
Progress on #7077 `until` is slow for two reasons: - It builds up years/months one by one, which is `O(n * O(surpasses))` - `surpasses` calls `new_balanced` a bunch of times, which is also ` O(n)` This helps fix the former: We start the years calculation at a minimum year bound that is close to but not over the final duration. It does a similar thing for months. This makes the years calculation constant time (it will at most check 2 more years). Because the months calculation is conservative (`min_years * 12`) it is still `O(n * O(surpasses))` in the months case for calendars with leap months, but the constant factor is greatly reduced. A per-calendar constant that produces a much closer bound might turn this into being constant time. I plan to try that out next. Note that when `largest_unit` is days or weeks we already optimize via RD. The days/weeks code below this doesn't need further optimization since it is already constant time in the outer algorithm (the days code will at most run 31 times). The inner `surpasses()` can also be improved by not redoing years/months calculations. @sffc has some ideas on how to do this without complicating the `surpasses` code too much, but I'm not exploring that here.
1 parent 5c3ad56 commit 6bd65a1

File tree

5 files changed

+195
-64
lines changed

5 files changed

+195
-64
lines changed

components/calendar/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ chrono_0_4 = ["dep:chrono"]
6868

6969
alloc = ["icu_locale_core/alloc", "tinystr/alloc", "serde?/alloc"]
7070

71+
[[bench]]
72+
name = "until"
73+
harness = false
74+
7175
[[bench]]
7276
name = "date"
7377
harness = false
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// This file is part of ICU4X. For terms of use, please see the file
2+
// called LICENSE at the top level of the ICU4X source tree
3+
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4+
5+
#[macro_export]
6+
macro_rules! bench_all_calendars {
7+
($group:ident, $bench_one_calendar:ident $($args:tt)*) => {
8+
$bench_one_calendar(&mut $group, "calendar/iso", icu::calendar::cal::Iso $($args)*);
9+
10+
$bench_one_calendar(
11+
&mut $group,
12+
"calendar/buddhist",
13+
icu::calendar::cal::Buddhist
14+
$($args)*
15+
);
16+
17+
$bench_one_calendar(&mut $group, "calendar/coptic", icu::calendar::cal::Coptic $($args)*);
18+
19+
$bench_one_calendar(
20+
&mut $group,
21+
"calendar/ethiopic",
22+
icu::calendar::cal::Ethiopian::new()
23+
$($args)*
24+
);
25+
26+
$bench_one_calendar(&mut $group, "calendar/indian", icu::calendar::cal::Indian $($args)*);
27+
28+
$bench_one_calendar(&mut $group, "calendar/julian", icu::calendar::cal::Julian $($args)*);
29+
30+
$bench_one_calendar(
31+
&mut $group,
32+
"calendar/chinese_cached",
33+
icu::calendar::cal::ChineseTraditional::new()
34+
$($args)*
35+
);
36+
37+
$bench_one_calendar(
38+
&mut $group,
39+
"calendar/gregorian",
40+
icu::calendar::cal::Gregorian
41+
$($args)*
42+
);
43+
44+
$bench_one_calendar(&mut $group, "calendar/hebrew", icu::calendar::cal::Hebrew $($args)*);
45+
46+
#[allow(deprecated)]
47+
$bench_one_calendar(
48+
&mut $group,
49+
"calendar/islamic/observational",
50+
icu::calendar::cal::Hijri::new_simulated_mecca()
51+
$($args)*
52+
);
53+
54+
$bench_one_calendar(
55+
&mut $group,
56+
"calendar/islamic/civil",
57+
icu::calendar::cal::Hijri::new_tabular(
58+
icu::calendar::cal::hijri::TabularAlgorithmLeapYears::TypeII,
59+
icu::calendar::cal::hijri::TabularAlgorithmEpoch::Friday,
60+
)
61+
$($args)*
62+
);
63+
64+
$bench_one_calendar(
65+
&mut $group,
66+
"calendar/islamic/ummalqura",
67+
icu::calendar::cal::Hijri::new_umm_al_qura()
68+
$($args)*
69+
);
70+
71+
$bench_one_calendar(
72+
&mut $group,
73+
"calendar/islamic/tabular",
74+
icu::calendar::cal::Hijri::new_tabular(
75+
icu::calendar::cal::hijri::TabularAlgorithmLeapYears::TypeII,
76+
icu::calendar::cal::hijri::TabularAlgorithmEpoch::Thursday,
77+
)
78+
$($args)*
79+
);
80+
};
81+
}

components/calendar/benches/convert.rs

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use criterion::{
77
};
88
use icu_calendar::{Calendar, Date};
99

10+
#[macro_use]
11+
mod common;
12+
1013
fn bench_calendar<C: Copy + Calendar>(
1114
group: &mut BenchmarkGroup<WallTime>,
1215
name: &str,
@@ -27,70 +30,7 @@ fn bench_calendar<C: Copy + Calendar>(
2730
fn convert_benches(c: &mut Criterion) {
2831
let mut group = c.benchmark_group("convert");
2932

30-
bench_calendar(&mut group, "calendar/iso", icu::calendar::cal::Iso);
31-
32-
bench_calendar(
33-
&mut group,
34-
"calendar/buddhist",
35-
icu::calendar::cal::Buddhist,
36-
);
37-
38-
bench_calendar(&mut group, "calendar/coptic", icu::calendar::cal::Coptic);
39-
40-
bench_calendar(
41-
&mut group,
42-
"calendar/ethiopic",
43-
icu::calendar::cal::Ethiopian::new(),
44-
);
45-
46-
bench_calendar(&mut group, "calendar/indian", icu::calendar::cal::Indian);
47-
48-
bench_calendar(&mut group, "calendar/julian", icu::calendar::cal::Julian);
49-
50-
bench_calendar(
51-
&mut group,
52-
"calendar/chinese_cached",
53-
icu::calendar::cal::ChineseTraditional::new(),
54-
);
55-
56-
bench_calendar(
57-
&mut group,
58-
"calendar/gregorian",
59-
icu::calendar::cal::Gregorian,
60-
);
61-
62-
bench_calendar(&mut group, "calendar/hebrew", icu::calendar::cal::Hebrew);
63-
64-
#[allow(deprecated)]
65-
bench_calendar(
66-
&mut group,
67-
"calendar/islamic/observational",
68-
icu::calendar::cal::Hijri::new_simulated_mecca(),
69-
);
70-
71-
bench_calendar(
72-
&mut group,
73-
"calendar/islamic/civil",
74-
icu::calendar::cal::Hijri::new_tabular(
75-
icu::calendar::cal::hijri::TabularAlgorithmLeapYears::TypeII,
76-
icu::calendar::cal::hijri::TabularAlgorithmEpoch::Friday,
77-
),
78-
);
79-
80-
bench_calendar(
81-
&mut group,
82-
"calendar/islamic/ummalqura",
83-
icu::calendar::cal::Hijri::new_umm_al_qura(),
84-
);
85-
86-
bench_calendar(
87-
&mut group,
88-
"calendar/islamic/tabular",
89-
icu::calendar::cal::Hijri::new_tabular(
90-
icu::calendar::cal::hijri::TabularAlgorithmLeapYears::TypeII,
91-
icu::calendar::cal::hijri::TabularAlgorithmEpoch::Thursday,
92-
),
93-
);
33+
bench_all_calendars!(group, bench_calendar);
9434

9535
group.finish();
9636
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// This file is part of ICU4X. For terms of use, please see the file
2+
// called LICENSE at the top level of the ICU4X source tree
3+
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4+
5+
use criterion::{
6+
black_box, criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
7+
};
8+
use icu_calendar::options::DateDifferenceOptions;
9+
use icu_calendar::types::{DateDurationUnit, RataDie};
10+
use icu_calendar::{Calendar, Date};
11+
12+
#[macro_use]
13+
mod common;
14+
15+
fn bench_calendar<C: Copy + Calendar>(
16+
group: &mut BenchmarkGroup<WallTime>,
17+
name: &str,
18+
calendar: C,
19+
unit: DateDurationUnit,
20+
) {
21+
// 300000 isn't a huge range, but these benchmarks are too slow otherwise.
22+
//
23+
// We can add more benchmarks once things are somewhat optimized
24+
let very_far_past = Date::from_rata_die(RataDie::new(-300_000_000), calendar);
25+
let far_past = Date::from_rata_die(RataDie::new(-300_000), calendar);
26+
let now = Date::from_rata_die(RataDie::new(0), calendar);
27+
let mut options = DateDifferenceOptions::default();
28+
options.largest_unit = Some(unit);
29+
group.bench_function(format!("{name}/far_past"), |b| {
30+
b.iter(|| black_box(far_past.try_until_with_options(&now, options).unwrap()))
31+
});
32+
// Still slow :/
33+
if unit != DateDurationUnit::Months {
34+
group.bench_function(format!("{name}/very_far_past"), |b| {
35+
b.iter(|| black_box(very_far_past.try_until_with_options(&now, options).unwrap()))
36+
});
37+
}
38+
}
39+
40+
fn convert_benches(c: &mut Criterion) {
41+
let mut group = c.benchmark_group("until/years");
42+
bench_all_calendars!(group, bench_calendar, DateDurationUnit::Years);
43+
group.finish();
44+
45+
let mut group = c.benchmark_group("until/months");
46+
bench_all_calendars!(group, bench_calendar, DateDurationUnit::Months);
47+
group.finish();
48+
49+
let mut group = c.benchmark_group("until/weeks");
50+
bench_all_calendars!(group, bench_calendar, DateDurationUnit::Weeks);
51+
group.finish();
52+
53+
let mut group = c.benchmark_group("until/days");
54+
bench_all_calendars!(group, bench_calendar, DateDurationUnit::Days);
55+
group.finish();
56+
}
57+
58+
criterion_group!(benches, convert_benches);
59+
criterion_main!(benches);

components/calendar/src/calendar_arithmetic.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,15 +934,39 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
934934
Ordering::Less => -1i64,
935935
};
936936

937+
// We don't want to spend time incrementally bumping it up one year
938+
// at a time, so let's pre-guess a year delta that is guaranteed to not
939+
// surpass.
940+
let year_diff = other.year().to_extended_year() - self.year().to_extended_year();
941+
let min_years = if year_diff == 0 {
942+
0
943+
} else {
944+
i64::from(year_diff) - sign
945+
};
946+
947+
debug_assert!(!self.surpasses(
948+
other,
949+
DateDuration::from_signed_ymwd(min_years, 0, 0, 0),
950+
sign,
951+
cal,
952+
));
953+
937954
// 1. Let _years_ be 0.
938955
// 1. If _largestUnit_ is ~year~, then
939956
// 1. Let _candidateYears_ be _sign_.
940957
// 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _candidateYears_, 0, 0, 0) is *false*,
941958
// 1. Set _years_ to _candidateYears_.
942959
// 1. Set _candidateYears_ to _candidateYears_ + _sign_.
960+
943961
let mut years = 0;
944962
if matches!(options.largest_unit, Some(DateDurationUnit::Years)) {
945963
let mut candidate_years = sign;
964+
if min_years != 0 {
965+
// Optimization: we start with min_years since it is guaranteed to not
966+
// surpass.
967+
candidate_years = min_years
968+
};
969+
946970
while !self.surpasses(
947971
other,
948972
DateDuration::from_signed_ymwd(candidate_years, 0, 0, 0),
@@ -953,6 +977,7 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
953977
candidate_years += sign;
954978
}
955979
}
980+
956981
// 1. Let _months_ be 0.
957982
// 1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
958983
// 1. Let _candidateMonths_ be _sign_.
@@ -965,6 +990,25 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
965990
Some(DateDurationUnit::Years) | Some(DateDurationUnit::Months)
966991
) {
967992
let mut candidate_months = sign;
993+
994+
if options.largest_unit == Some(DateDurationUnit::Months) && min_years != 0 {
995+
// Optimization: No current calendar supports years with month length < 12.
996+
// If something is at least N full years away, it is also at least 12*N full months away.
997+
//
998+
// In the future we can introduce per-calendar routines that are better at estimating a month count.
999+
//
1000+
// We only need to apply this optimization for largest_unit = Months. If the largest_unit is years then
1001+
// our candidate date is already pretty close and won't need more than 12 iterations to get there.
1002+
let min_months = min_years * 12;
1003+
debug_assert!(!self.surpasses(
1004+
other,
1005+
DateDuration::from_signed_ymwd(years, min_months, 0, 0),
1006+
sign,
1007+
cal,
1008+
));
1009+
candidate_months = min_months
1010+
}
1011+
9681012
while !self.surpasses(
9691013
other,
9701014
DateDuration::from_signed_ymwd(years, candidate_months, 0, 0),
@@ -1000,6 +1044,9 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
10001044
// 1. Set _days_ to _candidateDays_.
10011045
// 1. Set _candidateDays_ to _candidateDays_ + _sign_.
10021046
let mut days = 0;
1047+
// There is no pressing need to optimize candidate_days here: the early-return RD arithmetic
1048+
// optimization will be hit if the largest_unit is weeks/days, and if it is months or years we will
1049+
// arrive here with a candidate date that is at most 31 days off. We can run this loop 31 times.
10031050
let mut candidate_days = sign;
10041051
while !self.surpasses(
10051052
other,

0 commit comments

Comments
 (0)