Refactor Optimizer to use owned plans and TreeNode API (10% faster planning)#9948
Refactor Optimizer to use owned plans and TreeNode API (10% faster planning)#9948alamb merged 4 commits intoapache:mainfrom
Optimizer to use owned plans and TreeNode API (10% faster planning)#9948Conversation
613fdae to
b950a9e
Compare
Optimizer to use TreeNode API
b950a9e to
ef189fb
Compare
| // then run the optimizer with our custom rule | ||
| let optimizer = Optimizer::with_rules(vec![Arc::new(MyOptimizerRule {})]); | ||
| let optimized_plan = optimizer.optimize(&analyzed_plan, &config, observe)?; | ||
| let optimized_plan = optimizer.optimize(analyzed_plan, &config, observe)?; |
There was a problem hiding this comment.
This illustrates the API change -- the optimizer now takes an owned plan rather than a reference
| // analyze and optimize the logical plan | ||
| let plan = analyzer.execute_and_check(&plan, config.options(), |_, _| {})?; | ||
| optimizer.optimize(&plan, &config, |_, _| {}) | ||
| optimizer.optimize(plan, &config, |_, _| {}) |
There was a problem hiding this comment.
A large amount of this PR is changes to test to pass in an owned plan
|
|
||
| let formatted_plan = format!("{optimized_plan:?}"); | ||
| assert_eq!(formatted_plan, expected); | ||
| assert_eq!(plan.schema(), optimized_plan.schema()); |
There was a problem hiding this comment.
I changed the tests to call Optimizer::optimize directly, which already checks the schema doesn't change, so this test is redundant
This applies to several other changes in this PR
| /// We just need handle a subtree pattern itself. | ||
| /// Specifies how recursion for an `OptimizerRule` should be handled. | ||
| /// | ||
| /// Notice: **sometime** result after optimize still can be optimized, we need apply again. |
There was a problem hiding this comment.
I do not think this comment is applicable anymore -- the optimizer handles the recursion internally as well as applying multiple optimizer passes
| Ok(new_plan) | ||
| } | ||
|
|
||
| fn optimize_node( |
There was a problem hiding this comment.
This code implemented plan recursion within the optimizer and is (now) redundant with the TreeNode API
| Field { name: \"c\", data_type: UInt32, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} }], metadata: {} }, \ | ||
| field_qualifiers: [Some(Bare { table: \"test\" }), Some(Bare { table: \"test\" }), Some(Bare { table: \"test\" })], \ | ||
| functional_dependencies: FunctionalDependencies { deps: [] } }, \ | ||
| "Optimizer rule 'get table_scan rule' failed\n\ |
There was a problem hiding this comment.
The original error actually is incorrect that it reports the reversed schemas (the "new schema" was actually the original schema)
ef189fb to
d951289
Compare
d951289 to
a755338
Compare
Optimizer to use TreeNode APIOptimizer to use TreeNode API (10% faster planning)
52f3a54 to
2d5e154
Compare
2d5e154 to
a282d6a
Compare
a282d6a to
a4cb731
Compare
Optimizer to use TreeNode API (10% faster planning)Optimizer to use owned plans and TreeNode API (10% faster planning)
| } | ||
|
|
||
| /// Recursively rewrites LogicalPlans | ||
| struct Rewriter<'a> { |
There was a problem hiding this comment.
datafusion/optimizer/src/optimizer.rs has all the important changes (to use the TreeNode API and stop copying)
| &OptimizerContext::new(), | ||
| )? | ||
| .unwrap_or_else(|| plan.clone()); | ||
| let optimized_plan = |
There was a problem hiding this comment.
Since the optimizer correctly applies rules recursively now, there is no need to explicitly call optimize recursively
| )? | ||
| .unwrap_or_else(|| plan.clone()); | ||
| // Apply the rule once | ||
| let opt_context = OptimizerContext::new().with_max_passes(1); |
There was a problem hiding this comment.
Without this some of the tests don't pass. By default the optimizer runs a few times until no changes are detected. Limiting to 1 pass mimics the previous test behavior
There was a problem hiding this comment.
I'm not sure about this. It seems that the previous code does not limit the pass to 1. Why do we need to limit it now to have the same behavior as the previous one? 😕
There was a problem hiding this comment.
My explanation goes like: The loop that applies the rule more than once calls optimize_recursively each time
This test only called optimze_recursively once (directly) and thus the OptimizeRule is only applied once
When I rewrote the test to use Optimizer::optimize the loop will now kick in and so the OptimizeRule will be run several times unless we set with_max_passes
This same reasoning applies to the other tests, but apparently they get the same answer when applied more than once
|
@jackwener since you implemented some of the original optimizer recursion I wonder if you would have some time to review this PR |
jayzhan211
left a comment
There was a problem hiding this comment.
It looks pretty nice now!! 🚀
|
|
||
| let result = match rule.apply_order() { | ||
| // optimizer handles recursion | ||
| Some(apply_order) => new_plan.rewrite(&mut Rewriter::new( |
There was a problem hiding this comment.
Does rename it to rewrite_recurisvely more straightforward?
There was a problem hiding this comment.
It's a generic api (so using rewrite makes sense to me), it's not introduced by this PR
There was a problem hiding this comment.
I got a question here.
In previous code, optimize_inputs get the plan.inputs(). how does rewrite get the plan.inputs() here? How does childnode in rewrite equals to plan.inputs()
There was a problem hiding this comment.
Oh, I see the map_children
There was a problem hiding this comment.
Yes, exactly
(eventually) calls map_children: https://github.com/apache/arrow-datafusion/blob/cb21404bd3736ff9a6d8d443a67c64ece4c551a9/datafusion/common/src/tree_node.rs#L27-L31
This is the beauty of these TreeNode APIs and @peter-toth's plan to make them all consistent.
|
Thank you very much @jackwener and @jayzhan211 for the reviews 🙏 |
|
Thanks @alamb for this work. |
Note: this looks like a large PR, but many of the changes are lines that removed
&Which issue does this PR close?
Part of #9637 (stop copying
LogicalPlans in the Optimizer) and #8913 (unified TreeNode rewrite)Rationale for this change
The current structure of the
OptimizercopiesLogicalPlans a large number of times. This is both slow as well as requires a large number of allocationsAfter #9999, the
TreeNodeAPI can handle rewritingLogicalPlanefficiently withoutclone.Thus it makes sense to use the TreeNode API in the optimizer, both because I think the code is simpler as well as to take advantage of the performance improvements in TreeNode API.
What changes are included in this PR?
Optimizerto use TreeNode APIOptimizer::optimizeto take an owned LogicalPlan rather than force a copyAre these changes tested?
By existing CI
Performance benchmarks: Planning is 10% faster for TPCH, 13% faster for TPCDS
Details
Are there any user-facing changes?
There is a small API change:
Optimizer::optimizenow takes an ownedLogicalPlanrather a reference (which forces a copy)Planned follow on task