[CALCITE-7809] Avoid copying unchanged SQL and Rex operands - #5283
FrankChen021 wants to merge 1 commit into
Conversation
|
julianhyde
left a comment
There was a problem hiding this comment.
This is slop. Do not merge.
caicancai
left a comment
There was a problem hiding this comment.
This PR description looks very AI slop.
vlsi
left a comment
There was a problem hiding this comment.
Thanks for the change. I ran SqlValidatorTest, SqlToRelConverterTest, RelOptRulesTest, RexProgramTest, and RexShuttleTest on the PR head: 2444 tests, 0 failures. The approach is sound and the risk is low. A few things before merge:
- Nothing tests the property the PR adds. If
visitListgoes back to always copying, every existing test still passes, so the optimization can regress without anyone noticing. Could you add tests along these lines?RexShuttle.visitListon an unchangedImmutableListreturns the same instance and leavesupdate[0]false.- A shuttle that replaces a middle operand returns a new list with the prefix and suffix intact and sets
update[0]. This catches an off-by-one insubList(0, i). Replacing the first and the last operand covers the boundaries. - An unchanged input that is not an
ImmutableListstill comes back as anImmutableListwith equal elements. SqlShuttlereturns the sameSqlCallwhen no operand changes, returns a new call with the other operands kept as the same instances when one operand changes, and returns a new instance whenalwaysCopyis true and nothing changes.
- The measurement comes from Druid's benchmark, which nobody can rerun from this repository.
ubenchmarkalready hasRelNodeConversionBenchmark. A case with a largeARRAY[...]orINlist, run with-prof gc, would show the effect in Calcite itself and catch a regression later. - Scope: a plain
x IN ('1', ..., 'N')in Calcite (no Druid rewrite) goes throughSqlShuttle.visit(SqlNodeList), which still allocates a fullArrayListon every traversal.RexShuttle.visitArray,visitFieldCollations, and thevisitListoverrides inProjectFilterTransposeRuleandDateRangeRulesstill copy eagerly too. Either covervisit(SqlNodeList)here or say in the description that this PR targets only the call-operand paths. - The commit subject says
[CALCITE-7805], but this PR is CALCITE-7809. CALCITE-7805 is the umbrella allocation issue.
Minor, for the description: the "based directly on main at 38413ec" sentence only matters while the PR is open, and the arrow diagrams could each be one sentence.
| if (exprs instanceof ImmutableList) { | ||
| //noinspection unchecked | ||
| return (List<RexNode>) exprs; | ||
| } | ||
| return ImmutableList.copyOf(exprs); |
There was a problem hiding this comment.
ImmutableList.copyOf already returns its argument when that argument is a full ImmutableList, so the instanceof branch and the unchecked cast aren't needed:
return clonedOperands != null ? clonedOperands.build() : ImmutableList.copyOf(exprs);It also handles a case the current branch gets wrong: ImmutableList.subList(...) is an instanceof ImmutableList view that keeps the whole backing array reachable. copyOf compacts such a view, and this branch returns it as is. I checked both cases on Guava 33.4.8.
| if ((clonedOperand != operand) && (update != null)) { | ||
| update[0] = true; | ||
| if (clonedOperand != operand && clonedOperands == null) { | ||
| clonedOperands = ImmutableList.builder(); |
There was a problem hiding this comment.
Nit: ImmutableList.builderWithExpectedSize(exprs.size()) avoids growing the builder on the path where an operand changed.
| @@ -163,15 +163,30 @@ protected RexNode[] visitArray(RexNode[] exprs, boolean @Nullable [] update) { | |||
| */ | |||
| protected List<RexNode> visitList( | |||
There was a problem hiding this comment.
The method now can return exprs itself, and subclasses will start relying on that, so the Javadoc of this method should say it. Its @return is also wrong: the method returns a list, not an array. Suggestion:
* @return List of visited expressions; {@code exprs} itself if it is an
* {@link ImmutableList} and no expression was modified| @@ -100,24 +108,23 @@ public class SqlShuttle extends SqlBasicVisitor<@Nullable SqlNode> { | |||
| */ | |||
| protected class CallCopyingArgHandler implements ArgHandler<@Nullable SqlNode> { | |||
There was a problem hiding this comment.
The class Javadoc says the handler "deep-copies SqlCalls and their operands". That wasn't accurate before this PR and is less accurate now: the handler creates a new call only when an operand changes or when alwaysCopy is true. Since the PR touches this class, could you fix the sentence?
| final List<@Nullable SqlNode> operands = (List<@Nullable SqlNode>) call.getOperandList(); | ||
| this.clonedOperands = operands.toArray(new SqlNode[0]); | ||
| this.alwaysCopy = alwaysCopy; | ||
| this.clonedOperands = alwaysCopy ? copyOperands(call) : null; |
There was a problem hiding this comment.
Optional simplification: if result() copies the operands when clonedOperands is still null, the constructor needs no branch and alwaysCopy gets lazy copying too. When nothing changed, copying at result() gives the same array as copying here.
@Override public SqlNode result() {
if (!update && !alwaysCopy) {
return call;
}
final @Nullable SqlNode[] operands =
clonedOperands != null ? clonedOperands : copyOperands(call);
return call.getOperator().createCall(
call.getFunctionQuantifier(), call.getParserPosition(), operands);
}| clonedOperands[i] = newOperand; | ||
| return newOperand; | ||
| } | ||
|
|
There was a problem hiding this comment.
Nit: stray blank line before the closing brace.
@julianhyde @caicancai I don't accept the word 'slop' here. I deliberately guided the AI to generate the description, to include:
It may be long, but it's clear. I wrote an email in the dev mailing list, we can discuss about this(non-implementation details) in that thread. |
|
@vlsi Thank you very much for the feedback. I will address these problems later this week. |
From my point of view, it is hard to answer "why do we need the change at all?" question. Frank, I wonder if you could try this skill: https://github.com/Netcracker/qubership-ai-packages/tree/main/agent-packages/change-description-authoring From my point of view, it produces decent commit messages: #5278, #5230, #5213 |
That's a good suggestion. Let's talk about this case here, the "why" question I think is stated in the JIRA as: https://issues.apache.org/jira/browse/CALCITE-7809
Do you think the description is clear to answer the why ? |
|
@vlsi I updated the description, does it sound good to you? |
|
The updated description is better:
PS. The PR still misses calcite-level tests, and calcite-level benchmarks. If the PR had those tests, the description could be way better. |
Correct. I have not done this yet, it takes time. Thanks. |



Fixes CALCITE-7809.
Why
RexShuttleandSqlShuttlecopy operand collections before they know whether the traversal will replace any operand. Read-only traversals therefore allocate and discard copies of unchanged operands, which adds memory pressure for large expressions.In Druid's string-IN planning benchmark, this change reduced allocation by 2.72% at 100,000 literals and 1.55% at 1,000,000 literals. Timing confidence intervals overlapped, so no latency improvement is claimed.
What
Before this change:
This change makes the operand collections copy-on-write:
RexShuttle.visitListcreates its immutable-list builder only when the first child changes. An unchanged non-immutable input is still copied to preserve the immutable-result contract.SqlShuttlecreates its operand array only when a child changes, while preserving the explicitalwaysCopybehavior.The visitors still traverse every operand; this change does not skip validation, type inference, or conversion.
Verification
./gradlew :core:compileJava :core:compileTestJava./gradlew :core:test --tests org.apache.calcite.test.RexShuttleTest./gradlew :core:autostyleJavaCheck :core:checkstyleAllInPlanningBenchmark.queryStringInSqlPlanOnlywith-prof gcDruid benchmark results
Configuration:
inSubQueryThreshold=2147483647,rowsPerSegment=500000, 2 forks, 2 one-second warmup iterations, and 5 one-second measurement iterations. Allocation is cumulative bytes per operation, not retained or peak heap.The focused identity and boundary tests requested in review, and a Calcite-local
ubenchmark, are not yet included.Scope
This change covers
RexShuttle.visitListandSqlShuttlecall operands. It does not changeSqlShuttle.visit(SqlNodeList),RexShuttle.visitArray,visitFieldCollations, or thevisitListoverrides inProjectFilterTransposeRuleandDateRangeRules.