Skip to content

Conversation

lawnjelly
Copy link
Member

@lawnjelly lawnjelly commented Jun 19, 2025

Now that these node types are using LocalVector for children, we have the opportunity to speed up child access in bottleneck loops by using the unguarded access and saving needless ERR_FAIL_INDEX checks.

Really inside hot loops, there is no good reason to use the [] operator in LocalVector (which contains a CRASH_COND range check). We have already checked the size condition at the start of the loop, so it should never trigger.

There are previously two ways of doing unguarded access to elements:

  1. Call ptr() before the loop, and get a raw pointer which can be dereferenced during the loop
  2. Call ptr() each time within the loop, and deference during the loop

(1) is fine when the size of the LocalVector can be guaranteed never to change during iteration.
(2) is safer when the vector may be resized.

The primary reason for (2) is that during a resize, the base address of the data may change (during a grow or shrink). A resize will also potentially invalidate iterators (used in 4.x, but not present in 3.x) and cause crashes / UB, so using 4.x style iterators is not an option here for (2).

get_unchecked()

Although I began this PR using the vector.ptr()[..] type syntax for (2), I thought it would be likely nicer to add syntactic sugar here and add a wrapper function. There are several advantages of a wrapper:

  • The intention is clearer (from the wording)
  • The syntax is clearer (no ()[] which is not something you see so commonly in c++)
  • We can add DEV_ENABLED checks to prevent errors creeping in during development

This PR

In this PR I add some cases where we can use either method (1) above, or the get_unchecked() version.

Notes

  • I've also removed children_lock from Spatial as it is unused.

Things to watch for in LocalVector iteration in general

  1. If the vector changes data address during the iteration (via e.g. resize).
    Any stored raw pointers / iterators may become invalid.
  2. If the number of children changes during iteration.
    Any local num_children stored outside the loop becomes invalid. Getting the num_children each time is safer, although may not be optimized out in all cases versus storing in a local variable.
  3. Multithread access.
    There is no thread protection in LocalVector.

Copy link
Member

@Ivorforce Ivorforce left a comment

Choose a reason for hiding this comment

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

For me this is more of an indicator to use iterators instead.

Iterators use ptr iteration (or should at least, not sure where 3.x is at), and so are optimal on that front. But we don't need to do the manual 'ptr()' dance to avoid redundant index checks.

@lawnjelly
Copy link
Member Author

lawnjelly commented Jun 22, 2025

For me this is more of an indicator to use iterators instead.

As I said before, I hate iterators with a passion, I prefer to see exactly what is happening.

Many of us older gen prefer to use c++ as c with classes, and minimize the overly clever c++ stuff. Juan has written quite a bit on this subject, and keeping the codebase accessible as much as possible has been important in allowing contributors to get into it as easily as possible.

3.x hardly uses any iterators, and I'm keen to keep it that way. If the project as a whole were to insist on iterators, I would move on to a different project.

I did originally add a function get_unchecked() to get around the awkward ptr()[] syntax, maybe that would be better.

@lawnjelly lawnjelly force-pushed the faster_children_iteration branch 2 times, most recently from 53c7ee7 to aa3dc85 Compare June 22, 2025 16:52
@lawnjelly lawnjelly requested a review from Calinou July 1, 2025 16:52
@lawnjelly lawnjelly force-pushed the faster_children_iteration branch from aa3dc85 to cd9713e Compare July 16, 2025 02:08
@lawnjelly
Copy link
Member Author

lawnjelly commented Jul 16, 2025

Iterators use ptr iteration (or should at least, not sure where 3.x is at), and so are optimal on that front. But we don't need to do the manual 'ptr()' dance to avoid redundant index checks.

Indeed we don't have iterators in 3.x for LocalVector.

There is also a potential problem with the current iterators in 4.x (and why we can't use it instead of get_unchecked()) - that it will crash / cause UB if the vector is resized during the iteration.

Copy link
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

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

Tested locally, it works as expected. Code looks good to me.

@Calinou
Copy link
Member

Calinou commented Sep 30, 2025

Is this PR applicable to 4.x, or should we go for a different solution there?

@lawnjelly lawnjelly merged commit 3f70462 into godotengine:3.x Oct 1, 2025
14 checks passed
@lawnjelly lawnjelly deleted the faster_children_iteration branch October 1, 2025 04:18
@lawnjelly
Copy link
Member Author

Thanks!

@lawnjelly
Copy link
Member Author

lawnjelly commented Oct 1, 2025

Is this PR applicable to 4.x, or should we go for a different solution there?

I did move 4.x to use LocalVector in #107481 which recently got merged, which is the lions share of the win. It's possible that iteration could be more efficient in 4.x too, although that might depend on @Ivorforce as he prefers iterators (and 4.x is too far too gone to save on this front 😁 ). I'll take a look and see if there's an obvious equivalent there.

@vmarangozov
Copy link

For me this is more of an indicator to use iterators instead.

As I said before, I hate iterators with a passion, I prefer to see exactly what is happening.

Many of us older gen prefer to use c++ as c with classes, and minimize the overly clever c++ stuff.

+1.
+2 if older gen is a separate count.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants