-
Notifications
You must be signed in to change notification settings - Fork 467
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
Reorganize EquivalenceClasses to use more efficient algorithms #30082
base: main
Are you sure you want to change the base?
Reorganize EquivalenceClasses to use more efficient algorithms #30082
Conversation
1503d49
to
636c143
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.
Looks fine, but I'll defer the final say to @ggevay. I left some minor comments inside, there seems to be some unused code.
|
||
/// Returns a map that can be used to replace (sub-)expressions. | ||
pub fn reducer(&self) -> BTreeMap<&MirScalarExpr, &MirScalarExpr> { | ||
self.classes | ||
.iter() | ||
.flat_map(|c| c.iter().map(move |e| (e, &c[0]))) | ||
.collect() | ||
} |
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.
reducer
seems to be unused?
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.
It's meant to be used in the near future, but we can remove for this commit.
let remap_ref = &remap; | ||
self.classes | ||
.iter() | ||
.all(|c| c.iter().all(move |e| remap_ref.get(e) == Some(&c[0]))) |
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.
let remap_ref = &remap; | |
self.classes | |
.iter() | |
.all(|c| c.iter().all(move |e| remap_ref.get(e) == Some(&c[0]))) | |
self.classes | |
.iter() | |
.all(|c| c.iter().all(|e| remap.get(e) == Some(&c[0]))) |
pub fn reducer(&self) -> BTreeMap<&MirScalarExpr, &MirScalarExpr> { | ||
self.classes | ||
.iter() | ||
.flat_map(|c| c.iter().map(move |e| (e, &c[0]))) |
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.
.flat_map(|c| c.iter().map(move |e| (e, &c[0]))) | |
.flat_map(|c| c.iter().map(|e| (e, &c[0]))) |
Started a Nightly: https://buildkite.com/materialize/nightly/builds/10061 Edit: The lint failure is unrelated to this PR. Edit 2: Ok, I think that's a successful Nightly. It's showing a benchmark regression, but it's probably just a flake. I can't imagine that query to be significantly affected by this PR. The query is
|
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.
Thanks for working on this! I wrote some comments.
@@ -525,9 +520,18 @@ impl EquivalenceClasses { | |||
self.classes.extend(to_add); | |||
|
|||
// Tidy up classes, restore representative. | |||
// Specifically, we want to remove literal errors before restoring the equvialence class structure. |
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.
Typo in "equvialence"
simplified = simplified || self.replace(expr); | ||
simplified | ||
} | ||
fn reduce_child(&self, expr: &mut MirScalarExpr) -> bool { |
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.
This seems to be a duplicate code fragment: there is an other, identical reduce_child
in the same file. It's not on the trait, though, so self
is different, so maybe this is intentional? I'm not sure.
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.
Right, so it is different, and it is intentional. This method, and the others in the trait, are meant to sub in for those methods on Equivalences. However, we haven't pivoted the whole codebase over from Equivalences::replace to ExpressionReducer::replace. Internally, minimize_once uses the ExpressionReducer version, but externally (e.g. in EquivalencePropagation) the interface hasn't changed yet (the first goal is to improve minimize rather than uses of Equivalences).
// TODO: remove these measures once we are more confident about idempotence. | ||
let prev = self.clone(); | ||
self.minimize_once(&columns); | ||
mz_ore::soft_assert_eq_or_log!(self, &prev, "Equivalences::minimize() not idempotent"); |
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'm feeling a bit uneasy about removing this check just in the same PR where the stability check in minimize_once
is also being changed.
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.
Ok, this is fine after checking the new stability check in more detail, see #30082 (comment)
self.tidy(); | ||
|
||
stable | ||
// The state is stable if every expression is present in `remap` with the same representative, and their sizes are the same. |
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.
Can you explain the advantage of this new stability check compared to the old code that simply set stable = false
when a change happens? minimize_once
does a lot of stuff, it seems non-trivial to me to verify that this new check achieves the same as the old code. (Also, the old check is, I guess, faster.)
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.
The old check required, among other things, defensive copies of every expression, because MSE::reduce
does not produce a bool
result indicating whether a change happened. So we were already cloning everything and doing equality checks on them. The PR moves that cloning into the formation of remap
, independently needed for efficient operation, but since we now have a copy of the ground truth we started with, we use it rather than make defensive copies and bespoke change tracking.
it seems non-trivial to me to verify that this new check achieves the same as the old code.
100%. The new check is more likely to achieve the correct answer, and the complexity is in verifying that the old code even did that in the first place.
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, eliminating the defensive copies is a good point!
When you say that the new code's answer is more correct, you mean that the old code might have sometimes set stable = false
even when we were actually already stable, due to making some change that was not actually a meaningful change?
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.
Ok, the new check seems correct, indeed.
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.
When you say that the new code's answer is more correct ...
Imo it's not about the answer as much as the process. The new code almost directly confirms that the contents of classes
at the start and at the end are identical. The old code had bespoke change tracking that relied on some amount of care in the implementation. It will be harder for the new code to produce a wrong answer going forward, but the old code could pretty easily do that.
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.
Looks good, modulo the very minor outstanding comments.
Edit: There are significant changes in the meantime. Will do another review round now.
636c143
to
08d6946
Compare
This PR updates
EquivalenceClasses
to form and use a map from expressions to expressions when it attempts to simplify its own expressions. Minimization starts with aself.tidy()
to ensure the equivalence classes are equivalence relations, in fact as well as in name. It then repeatedly callsminimize_once
, which does:For me, this is much simpler algorithmically, vs wobbling the stages around. It also has the advantage that we can compare the map from step 1. with the equivalence classes in step 4., and if they are identical we are done. No need to do the extra round of validation.
There isn't much improvement in running times on the incident 217 reproduction. The outlier times for
minimize()
calls go down by ~4x (in debug builds from ~320ms to ~80ms), but they seem to not be as impactful as the overall work done on the query. There are several additional optimizations that are available, though:reduce_expr
that would be served (linearly) by the equivalence classes.I'm not sure how much more we'd sneak out of this without some more careful thought. It's not much more than linear time at the moment, and we need to take linear time at least just to look at and record the classes. The larger problem seems to be around needing to do the work at all, and figuring how to not show up with 6k+ expressions that are variously equated (in the repro). The query optimizes in about 7s on a release build, but no more than a few milliseconds are taken in any call to
minimize()
.Motivation
Tips for reviewer
Checklist
$T ⇔ Proto$T
mapping (possibly in a backwards-incompatible way), then it is tagged with aT-proto
label.