Repository navigation
Description
Is your feature request related to a problem or challenge?
Broken out from #9577 where @mustafasrepo @comphead and @jayzhan211 and I were discussing optimizer performance
TLDR is that the datafusion optimizer is slow. When I did some profiling locally by running the following
cargo bench --bench sql_planner -- physical_plan_tpch_allMy analysis is that almost 40% of the planning time is spent in SimplifyExprs and CommonSubexprEliminate and most of that time is related to copying expressions from what I can tell
While those passes themselves internally make a bunch of clones, which we are improving (e.g. @jayzhan211 on #9628) I think there is a more fundamental structural problem
I think a core challenge is that the OptimizerRule trait pretty much requires copying Exprs on each pass, as it gets a &LogicalPlan input, but produces a LogicalPlan output
// Required methods
fn try_optimize(
&self,
plan: &LogicalPlan,
config: &dyn OptimizerConfig
) -> Result<Option<LogicalPlan>, DataFusionError>;This mean any pass that works on Exprs must clone all Exprs (by calling LogicalPlan::expressions()) rewrite them, and then then create a new LogicalPlan with those new Exprs.
Here is that pattern in the expression simplifier:
Describe the solution you'd like
Find some way to avoid clone'ing exprs during LogicalPlan rewrite
Update: here are the tasks:
Infrastructure Preparation
- Refactor
Optimizerto use owned plans andTreeNodeAPI (10% faster planning) #9948 - Avoid
LogicalPlan::clone()inLogicalPlan::map_childrenwhen possible #9999
Update OptimizerRules to avoid copying
-
SimplifyExpressions: IntroduceOptimizerRule::rewriteto rewrite in place, rewriteExprSimplifier(20% faster planning) #9954 -
CommonSubexprEliminate: Stop copyingExprs and LogicalPlans so much during Common Subexpression Elimination #9873 -
DecorrelatePredicateSubquery: Stop copying LogicalPlan and Exprs inDecorrelatePredicateSubquery#10289 -
EliminateCrossJoin: Stop copying LogicalPlan and Exprs inEliminateCrossJoin#10287 -
EliminateDuplicatedExpr: refactorEliminateDuplicatedExproptimizer pass to avoid clone #10218 -
EliminateFilter: Stop copying LogicalPlan and Exprs inEliminateFilter#10288 -
EliminateJoin: Implement rewrite for EliminateOneUnion and EliminateJoin #10184 -
EliminateLimit: Stop copying LogicalPlan and Exprs inEliminateLimit#10212 -
EliminateNestedUnion: Stop copying LogicalPlan and Exprs inEliminateNestedUnion#10296 -
EliminateOneUnion: Implement rewrite for EliminateOneUnion and EliminateJoin #10184 -
EliminateOuterJoin: RefactorEliminateOuterJointo implementOptimizerRule::rewrite()#10081 -
ExtractEquijoinPredicate: implement rewrite for ExtractEquijoinPredicate and avoid clone in filter #10165 -
FilterNullJoinKeysimplement rewrite for FilterNullJoinKeys #10166 -
OptimizeProjections: Stop copying LogicalPlan and Exprs inOptimizeProjections#10209 -
PropagateEmptyRelation: Stop copying LogicalPlan and Exprs inPropagateEmptyRelation#10290 -
PushDownFilter: Stop copying LogicalPlan and Exprs inPushDownFilter#10291 -
PushDownLimit: Stop copying LogicalPlan and Exprs inPushDownLimit#10292 -
ReplaceDistinctWithAggregate: Stop copying LogicalPlan and Exprs inReplaceDistinctWithAggregate#10293 -
RewriteDisjunctivePredicate: Stop copying LogicalPlan and Exprs inRewriteDisjunctivePredicate#10213 -
ScalarSubqueryToJoin: Stop copying LogicalPlan and Exprs inScalarSubqueryToJoin#10294 -
SingleDistinctToGroupBy: Stop copying LogicalPlan and Exprs inSingleDistinctToGroupBy#10295 -
UnwrapCastInComparisonRefactorUnwrapCastInComparisonto implementOptimizerRule::rewrite()#10087 / RefactorUnwrapCastInComparisonto removeExprclones #10115
Update AnalyzerRules to avoid copying
-
AnalyzerMisc: Minor: Avoid copying all expressions inAnalzyer/check_plan#9974 -
InlineTableScan: Avoid copies inInlineTableScanvia TreeNode API #10038 -
ApplyFunctionRewrites: fixNamedStructField should be rewritten in OperatorToFunctionin subquery regression (changeApplyFunctionRewritesto use TreeNode API #10032 -
TypeCoercion: Stop copying LogicalPlan and Exprs inTypeCoercion#10210 -
TypeCoercionmore: Onyl recompute schema inTypeCoercionwhen necessary #10365 -
CountWildcardRule: (needs a little reworking to avoid clones) Avoid copies inCountWildcardRulevia TreeNode API #10066
Update Other to avoid copying
Describe alternatives you've considered
No response
Additional context
We have talked about various other ways to reduce copying of LogicalPlans as well as its challenges in other tickets:

Activity
jayzhan211 commentedon Mar 16, 2024
I have two idea before deep diving to the code
ScalarUDFImpl::simplfy()API, moveSimplifyInfoet al to datafusion_expr #9304 may help here too or we can rely onTransformed<Expr>if expr is Original, we can avoid clone.
alamb commentedon Mar 16, 2024
I think 2 sounds interesting
Another think I was thinking was something like
LogicalPlan::rewrite(mut self, rewriter)I think that along with
Arc::try_unwrapcould be used to minimize the places where cloning was actually neededMaybe we can prototype with
And then play around with the code that calls
try_optimizehere https://github.com/apache/arrow-datafusion/blob/f4107d47bb4c0260d301294ddfc7c67d96574636/datafusion/optimizer/src/optimizer.rs#L360-L368 to try and use thetry_optimize_ownedAPI (without having to rewrite all the optimizers) forSimplifyExprsIf we could show a significant performance improvement for the
sql_plannerbenchmarks then I think it would be worth spending time reworking the other APIsjayzhan211 commentedon Mar 17, 2024
@alamb I think being able to skip failed rules is the main challenge to rewriting the plan with ownership
https://github.com/apache/arrow-datafusion/blob/37253e57beb25f0f1a4412b75421a489c2cb3c6a/datafusion/optimizer/src/optimizer.rs#L325
We need to restore the original plan if the rewrite fails at some point, however, the old plan is consumed and we lose ownership. Giving up ownership too early is not a good idea if we need to restore the original plan.
Instead of Transformed, I think we need to preserve the old data and fail state to mimic Err in Result but with old data
Then, for every optimization, we return
Optimized<LogicalPlan>orOptimzied<Expr>alamb commentedon Mar 17, 2024
This is an excellent point
If we need to preserve the old data, there is no choice but to copy the
LogicalPlanon each pass 🤔 (well I guess there could be some way to repair the partially rewritten plan, but that sounds very compliated).Since
skip_failed_rulesisfalseby default:https://github.com/apache/arrow-datafusion/blob/dcfe70987e98da0410146b5e1292ab20f3f118e0/datafusion/common/src/config.rs#L549
Could we maybe only do the copy when
skip_failed_rulesis enabled? Something like this:jayzhan211 commentedon Mar 17, 2024
I think it is possible to preserve unnecessary clones even if we preserve the old plan, and only clone the data for the new expr and new plan, but I agree it is a little complicated, it needs the enum I show above. We can optimize for the default path first, and others later on.
alamb commentedon Mar 17, 2024
Let's give it a try!
jayzhan211 commentedon Mar 17, 2024
note: I profile my change and find out the time does not improve at all, I find that the cloned inside
expr_list_to_fieldsmay be the core issue.jayzhan211 commentedon Mar 20, 2024
I analyze the
sql_planneragain and find thatexprlist_to_fieldsandcalc_func_dependencies_for_projectare the two most time spending func.so I think #9595 might be an important step for the optimization here.
https://github.com/apache/arrow-datafusion/blob/ad8d552b9f150c3c066b0764e84f72b667a649ff/datafusion/expr/src/logical_plan/plan.rs#L1804-L1813
alamb commentedon Mar 20, 2024
I played around with some ideas last night and it is looking promising (though not yet done). I put my draft here #9708. I hope to try and work on it more over the next few days, but I am pretty busy preparing for meetups / presentations / papers for next week. It may be a few more days
jayzhan211 commentedon Mar 20, 2024
I would be surprised if #9708 improves, let me see if I can improve #9658 based on it.
alamb commentedon Mar 20, 2024
BTW here is a video of how I profile DataFusion: #9577 (comment)
universalmind303 commentedon Mar 20, 2024
I'm curious, It seems like most of the reasoning behind Arc<LogicalPlan> and
cloneare due to the optimizer stage. Polars uses an intermediate representationALogicalPlan(arena allocated logical plan) solely for the optimizer stage. I wonder if the same techinque could be applied here.alamb commentedon Mar 20, 2024
I think area allocating is pretty common in various compiler/query optimizer systems because you know the lifetime of a plan doesn't outlive the the optimizer pass (as in you can bound the intermediate results)
I think rust actually is ideally setup to avoid having to arena allocate (which still requires copying stuff to a new plan) (using ownership) -- we just need to regigger how DataFusion is setup to use this. I think we are close
jayzhan211 commentedon Mar 21, 2024
There is one issue that we are not able to call
Arc::into_innerfor skip-failed-rules path since we hold a clone for restoration.21 remaining items
peter-toth commentedon Apr 15, 2024
@alamb, @jayzhan211 this is another low hanging fruit to refactor
UnwrapCastInComparison: #10087Lordworms commentedon Apr 17, 2024
I am interested in refactoring some of the OptimizeRule, I'll start with some easy optimization rules
alamb commentedon Apr 19, 2024
Thanks @Lordworms -- that sounds great. There are now some good examples of the basic pattern (e.g #10081 and #10066)
alamb commentedon Apr 23, 2024
Update here is that I am hacking on getting
TypeCoercion(#10039) to avoid clones. It is turning out to be tricker than others as type coercion actually updates the types of the plans, so when rewriting expressions I need to find some way to re-calculate the schemas correctly. I am continuing to hack on that as @peter-toth works on CSE (#10067) and @Lordworms is cranking on some of the other passesalamb commentedon Apr 23, 2024
I also re-profiled the tpcds benchmarks via
And here is what it shows:

Thus I am going to take a peek to see if there is any low hanging fruit in
ApplyFunctionRewritesEliminateCrossJoin,OptimzieProjectionsandFilterPushdownalamb commentedon Apr 24, 2024
I filed some tickets to track the larger items #10209 #10210 #10211
Lordworms commentedon Apr 24, 2024
take eliminate_duplicated_expr and eliminate_cross_join
alamb commentedon Apr 24, 2024
FYI @Lordworms I took a look at brief look and they may be somewhat tricky. Of the two, i think elimnate_duplicated_expr is the easier
Lordworms commentedon Apr 24, 2024
Yes, actually I have finished the elimnate_duplicated_expr already, I'll merge them two as a single PR after finishing eliminate_cross_join
alamb commentedon Apr 24, 2024
If possible two single PRs would be easier to review and merge
Lordworms commentedon Apr 24, 2024
sure
[-]Stop copying `LogicalPlan` during OptimizerPasses[/-][+][EPIC] Stop copying `LogicalPlan` during OptimizerPasses[/+]alamb commentedon Apr 29, 2024
I filed tickets for the other optimizer passes and linked them to the description of this ticket
alamb commentedon May 17, 2024
Update here is that this issue is done other other than
SingleDistinctToGroupBy#10295 (which @appletreeisyellow is working on )Exprs and LogicalPlans so much during Common Subexpression Elimination #9873 which I will do (but I am currently waiting for @peter-toth 's work in RewriteCommonSubexprEliminateto avoid copies using TreeNode #10067 to complete)