Skip to content

Conversation

@idegtiarenko
Copy link
Contributor

This attempts to make optimizing a tiny bit cheaper.
Please see inline comments for details.

Related to: #124395

// to avoid allocating iterator that performs concurrent modification checks
for (int c = 0; c < children.size(); c++) {
children.get(c).forEachUp(action);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forEachDown and forEachUp do not seem to have visible impact on profile I reviewed, however replacing them to loops should make profiles outputs several stacks shorter, especially when those are nested.

Screenshot at 2025-03-19 15-21-23

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add this as a comment, please? The explanation in the comment helps, but I think the important point is that this reduces stack size rather than increasing performance, which the current comment kinda implies by mentioning the concurrent modification checks - which I understand didn't seem to be a problem?

// please do not refactor it to a for-each loop
// to avoid allocating iterator that performs concurrent modification checks
for (int i = 0; i < c.size(); i++) {
var e = c.get(i);
Copy link
Contributor Author

@idegtiarenko idegtiarenko Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayList.Itr is surprisingly visible in profile (those are blue rectangles on top left of the pink ones) when executing this code (maybe because it spends time checking for concurrent modifications?).
Replacing it with a for loop should make it a bit cheaper.

Screenshot at 2025-03-19 15-23-26

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're making more and more improvements to how we perform query plan transformations/traversals.

Could we please add at least a micro benchmark as one of the next steps? Without one, it's both hard to understand the impact of optimizations, and it's also unclear if we maybe introduce accidental regressions (in whichever PRs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will try to add one in a separate change.

// has no type info so it's difficult to have automatic checking without having base classes).

if (arg instanceof Collection<?> c) {
if (arg instanceof List<?> c) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please confirm we do not expect sets in here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't however it's not enforced - add an else throwing an error for safety.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ this could easily become a bug in the future, as it makes total sense for a node property to be e.g. a set of expressions. We might also just not have any test that exercises this code path with a non-list collection. To really know, we'd have to go and check all the info() method implementations on Node subclasses manually. And enforce that the only collection that node properties can use are lists.

My suggestion: let's add a code path for List<?> in addition to the more general code path for Collection<?> - and let's add an assertion in the latter because that code path shouldn't be used, so that this only trips in tests but doesn't damage production code.

children().forEach(c -> c.forEachDown(action));
// please do not refactor it to a for-each loop
// to avoid allocating iterator that performs concurrent modification checks
for (int c = 0; c < children.size(); c++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's squeeze a bit more perf:

for (int c = 0, s = children.size(); c < s; c++)

Copy link
Member

@costin costin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Please keep an eye out of stream usage - I've specifically avoided them in the core however they are used in some rules and they started showing up last I checked the profiler.

// has no type info so it's difficult to have automatic checking without having base classes).

if (arg instanceof Collection<?> c) {
if (arg instanceof List<?> c) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't however it's not enforced - add an else throwing an error for safety.

@costin
Copy link
Member

costin commented Mar 20, 2025

Being a small change, it makes sense to backport this all the way to 8.x, especially after seeing the list iterator bubbling up.

@idegtiarenko idegtiarenko marked this pull request as ready for review March 20, 2025 07:16
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@idegtiarenko idegtiarenko added auto-backport Automatically create backport pull requests when merged v8.19.0 labels Mar 20, 2025
Copy link
Contributor

@alex-spies alex-spies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ievgen, this generally LGTM. However, I think we should preserve the Collection code path in QueryPlan.doTransformExpression just in case that some node property is not a list. Alternatively, we could also double check and enforce that node properties that are collections can only be lists, and place that enforcement close to the actual node properties so it's possible to validate this invariant without relying on (potentially insufficient) testing.

A thought: the improvement to Node.java works because the children of a query plan are not mutable. I wonder if we could make this clearer/more idiomatic by turning the children into an unmodifiable list - which would also throw exceptions in case we accidentally don't respect this invariant somewhere.

// to avoid allocating iterator that performs concurrent modification checks
for (int c = 0; c < children.size(); c++) {
children.get(c).forEachUp(action);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add this as a comment, please? The explanation in the comment helps, but I think the important point is that this reduces stack size rather than increasing performance, which the current comment kinda implies by mentioning the concurrent modification checks - which I understand didn't seem to be a problem?

// please do not refactor it to a for-each loop
// to avoid allocating iterator that performs concurrent modification checks
for (int i = 0; i < c.size(); i++) {
var e = c.get(i);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're making more and more improvements to how we perform query plan transformations/traversals.

Could we please add at least a micro benchmark as one of the next steps? Without one, it's both hard to understand the impact of optimizations, and it's also unclear if we maybe introduce accidental regressions (in whichever PRs).

// has no type info so it's difficult to have automatic checking without having base classes).

if (arg instanceof Collection<?> c) {
if (arg instanceof List<?> c) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ this could easily become a bug in the future, as it makes total sense for a node property to be e.g. a set of expressions. We might also just not have any test that exercises this code path with a non-list collection. To really know, we'd have to go and check all the info() method implementations on Node subclasses manually. And enforce that the only collection that node properties can use are lists.

My suggestion: let's add a code path for List<?> in addition to the more general code path for Collection<?> - and let's add an assertion in the latter because that code path shouldn't be used, so that this only trips in tests but doesn't damage production code.


return hasChanged ? transformed : arg;
}
assert arg instanceof Set<?> == false : "Set arguments are not supported";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is insufficient in theory - someone in the future could use a Queue (for whatever reason) or a custom implementation of Collection. A safer way is to just assert false here in case of Collection and saying that non-list collections are not allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Let me change this to Collection<?>.
I would like to avoid another branch with custom logic for collection for now as somebody might accidentally use it without knowing its performance limitation.
We can add it when we see the set or any other custom implementation is absolutely required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline: in practical terms, yeah that'd likely be fine. But I'm also paranoid as this is called on general node properties and for whatever reason there might just be one node subclass that never hit this code path in tests, yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this in person and decided to keep the other branch for now (in case this behaviour is actually used but not covered by tests).

@idegtiarenko idegtiarenko requested a review from alex-spies March 20, 2025 12:11
Copy link
Contributor

@alex-spies alex-spies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ievgen, LGTM!

action.accept((T) this);
children().forEach(c -> c.forEachDown(action));
// please do not refactor it to a for-each loop to avoid
// allocating iterator that performs concurrent modification checks and extra stack traces
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// allocating iterator that performs concurrent modification checks and extra stack traces
// allocating iterator that performs concurrent modification checks and extra stack frames

}
return hasChanged ? transformed : arg;
} else if (arg instanceof Collection<?> c) {
List<Object> transformed = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to mark this branch as deprecated/likely dead + we could still add an assert false here - before maybe later getting rid of this branch altogether and enforcing that the only collections used as properties are all lists (but in a more visible place, e.g. in the c'tor of QueryPlan/Node).

@idegtiarenko idegtiarenko merged commit 9ddcede into elastic:main Mar 21, 2025
17 checks passed
@elasticsearchmachine
Copy link
Collaborator

💚 Backport successful

Status Branch Result
9.0
8.x

idegtiarenko added a commit to idegtiarenko/elasticsearch that referenced this pull request Mar 21, 2025
@idegtiarenko idegtiarenko deleted the 124395 branch March 21, 2025 08:23
afoucret pushed a commit to afoucret/elasticsearch that referenced this pull request Mar 21, 2025
smalyshev pushed a commit to smalyshev/elasticsearch that referenced this pull request Mar 21, 2025
omricohenn pushed a commit to omricohenn/elasticsearch that referenced this pull request Mar 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged >non-issue Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.19.0 v9.0.0 v9.1.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants